lex-frame-semantics 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: a2fd7637ea32eaf9de85020e859dfd65525545fcde99f19b1ec0e6f83caa248e
4
+ data.tar.gz: 7127f8ba47ae7f06f339c62586bebbe44389d338a3a3a990faca109357517451
5
+ SHA512:
6
+ metadata.gz: c979ec6a48383fa8b5b1c34af874d011ee9cfe1ffd7f6c5a7eb91921f88643474da0ae9ee860d048c414c80a6d5b4d84a39fdd309f01115356b3842f323256b7
7
+ data.tar.gz: c2516c54bac8155ac8209c5b3579eb393b3147a55d686a516c57bd285ab647ce933bb4b73facb6de5a18d1529748bf8db6d7e32574741cf5ae8ea2596a7dfbaf
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/frame_semantics/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-frame-semantics'
7
+ spec.version = Legion::Extensions::FrameSemantics::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Frame Semantics'
12
+ spec.description = "Fillmore's Frame Semantics engine for LegionIO — conceptual frame activation, slot filling, and instance creation"
13
+ spec.homepage = 'https://github.com/LegionIO/lex-frame-semantics'
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-frame-semantics'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-frame-semantics'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-frame-semantics'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-frame-semantics/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-frame-semantics.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FrameSemantics
6
+ module Helpers
7
+ class Client
8
+ include Runners::FrameSemantics
9
+
10
+ private
11
+
12
+ def engine
13
+ @engine ||= FrameEngine.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FrameSemantics
6
+ module Helpers
7
+ SLOT_TYPES = %i[core peripheral extra_thematic].freeze
8
+ FRAME_RELATIONS = %i[inherits_from is_inherited_by uses is_used_by subframe_of has_subframe].freeze
9
+ ACTIVATION_LABELS = {
10
+ (0.8..) => :dominant,
11
+ (0.6...0.8) => :active,
12
+ (0.4...0.6) => :primed,
13
+ (0.2...0.4) => :latent,
14
+ (..0.2) => :inactive
15
+ }.freeze
16
+
17
+ MAX_FRAMES = 150
18
+ MAX_INSTANCES = 500
19
+ MAX_HISTORY = 500
20
+ DEFAULT_ACTIVATION = 0.3
21
+ ACTIVATION_BOOST = 0.15
22
+ ACTIVATION_DECAY = 0.03
23
+ SLOT_FILL_BOOST = 0.1
24
+ COMPLETION_THRESHOLD = 0.7
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module FrameSemantics
8
+ module Helpers
9
+ class Frame
10
+ attr_reader :id, :name, :domain, :slots, :relations, :activation, :activation_count, :created_at
11
+
12
+ def initialize(name:, domain:)
13
+ @id = SecureRandom.uuid
14
+ @name = name
15
+ @domain = domain
16
+ @slots = {}
17
+ @relations = []
18
+ @activation = DEFAULT_ACTIVATION
19
+ @activation_count = 0
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def add_slot(name:, slot_type: :core, required: true)
24
+ @slots[name] = { type: slot_type, filler: nil, required: required }
25
+ self
26
+ end
27
+
28
+ def fill_slot(name:, filler:)
29
+ return false unless @slots.key?(name)
30
+
31
+ @slots[name][:filler] = filler
32
+ @activation = [@activation + SLOT_FILL_BOOST, 1.0].min
33
+ true
34
+ end
35
+
36
+ def clear_slot(name:)
37
+ return false unless @slots.key?(name)
38
+
39
+ @slots[name][:filler] = nil
40
+ true
41
+ end
42
+
43
+ def core_slots
44
+ @slots.select { |_k, v| v[:type] == :core }
45
+ end
46
+
47
+ def filled_slots
48
+ @slots.reject { |_k, v| v[:filler].nil? }
49
+ end
50
+
51
+ def completion_ratio
52
+ cs = core_slots
53
+ return 0.0 if cs.empty?
54
+
55
+ filled_core = cs.count { |_k, v| !v[:filler].nil? }
56
+ filled_core.to_f / cs.size
57
+ end
58
+
59
+ def complete?
60
+ completion_ratio >= COMPLETION_THRESHOLD
61
+ end
62
+
63
+ def activate!
64
+ @activation = [@activation + ACTIVATION_BOOST, 1.0].min
65
+ @activation_count += 1
66
+ self
67
+ end
68
+
69
+ def decay!
70
+ @activation = [@activation - ACTIVATION_DECAY, 0.0].max
71
+ self
72
+ end
73
+
74
+ def activation_label
75
+ ACTIVATION_LABELS.each do |range, label|
76
+ return label if range.cover?(@activation)
77
+ end
78
+ :inactive
79
+ end
80
+
81
+ def add_relation(relation:, target_frame_id:)
82
+ @relations << { relation: relation, target_frame_id: target_frame_id }
83
+ self
84
+ end
85
+
86
+ def to_h
87
+ {
88
+ id: @id,
89
+ name: @name,
90
+ domain: @domain,
91
+ slots: @slots,
92
+ relations: @relations,
93
+ activation: @activation,
94
+ activation_label: activation_label,
95
+ activation_count: @activation_count,
96
+ completion_ratio: completion_ratio,
97
+ complete: complete?,
98
+ created_at: @created_at
99
+ }
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FrameSemantics
6
+ module Helpers
7
+ class FrameEngine
8
+ def initialize
9
+ @frames = {}
10
+ @instances = []
11
+ end
12
+
13
+ def create_frame(name:, domain:, slots: {})
14
+ frame = Frame.new(name: name, domain: domain)
15
+ slots.each do |slot_name, opts|
16
+ frame.add_slot(
17
+ name: slot_name,
18
+ slot_type: opts.fetch(:type, :core),
19
+ required: opts.fetch(:required, true)
20
+ )
21
+ end
22
+ prune_frames_if_full
23
+ @frames[frame.id] = frame
24
+ frame
25
+ end
26
+
27
+ def define_slot(frame_id:, name:, slot_type: :core, required: true)
28
+ frame = @frames[frame_id]
29
+ return nil unless frame
30
+
31
+ frame.add_slot(name: name, slot_type: slot_type, required: required)
32
+ frame
33
+ end
34
+
35
+ def fill_slot(frame_id:, slot_name:, filler:)
36
+ frame = @frames[frame_id]
37
+ return false unless frame
38
+
39
+ result = frame.fill_slot(name: slot_name, filler: filler)
40
+ frame.activate! if result
41
+ result
42
+ end
43
+
44
+ def instantiate_frame(frame_id:, context:, confidence: 0.7)
45
+ frame = @frames[frame_id]
46
+ return nil unless frame
47
+
48
+ fillers = frame.slots.transform_values { |v| v[:filler] }
49
+ instance = FrameInstance.new(
50
+ frame_id: frame_id,
51
+ frame_name: frame.name,
52
+ slot_fillers: fillers,
53
+ context: context,
54
+ confidence: confidence
55
+ )
56
+ @instances << instance
57
+ @instances.shift while @instances.size > MAX_INSTANCES
58
+ instance
59
+ end
60
+
61
+ def add_frame_relation(frame_id:, relation:, target_frame_id:)
62
+ frame = @frames[frame_id]
63
+ return false unless frame
64
+ return false unless FRAME_RELATIONS.include?(relation)
65
+
66
+ frame.add_relation(relation: relation, target_frame_id: target_frame_id)
67
+ true
68
+ end
69
+
70
+ def activate_frame(frame_id:)
71
+ frame = @frames[frame_id]
72
+ return false unless frame
73
+
74
+ frame.activate!
75
+ true
76
+ end
77
+
78
+ def active_frames
79
+ @frames.values.select { |f| f.activation > 0.5 }
80
+ end
81
+
82
+ def frames_by_domain(domain:)
83
+ @frames.values.select { |f| f.domain == domain }
84
+ end
85
+
86
+ def related_frames(frame_id:)
87
+ frame = @frames[frame_id]
88
+ return [] unless frame
89
+
90
+ frame.relations.filter_map { |rel| @frames[rel[:target_frame_id]] }
91
+ end
92
+
93
+ def most_activated(limit: 5)
94
+ @frames.values.sort_by { |f| -f.activation }.first(limit)
95
+ end
96
+
97
+ def instances_for_frame(frame_id:)
98
+ @instances.select { |i| i.frame_id == frame_id }
99
+ end
100
+
101
+ def complete_frames
102
+ @frames.values.select(&:complete?)
103
+ end
104
+
105
+ def decay_all
106
+ @frames.each_value(&:decay!)
107
+ end
108
+
109
+ def prune_inactive
110
+ @frames.reject! { |_id, f| f.activation <= 0.05 }
111
+ end
112
+
113
+ def to_h
114
+ {
115
+ frame_count: @frames.size,
116
+ instance_count: @instances.size,
117
+ active_count: active_frames.size,
118
+ complete_count: complete_frames.size,
119
+ domains: @frames.values.map(&:domain).uniq.sort
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ def prune_frames_if_full
126
+ return unless @frames.size >= MAX_FRAMES
127
+
128
+ oldest = @frames.values.min_by(&:activation)
129
+ @frames.delete(oldest.id) if oldest
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module FrameSemantics
8
+ module Helpers
9
+ class FrameInstance
10
+ attr_reader :id, :frame_id, :frame_name, :slot_fillers, :context, :confidence, :created_at
11
+
12
+ def initialize(frame_id:, frame_name:, slot_fillers:, context:, confidence: 0.7)
13
+ @id = SecureRandom.uuid
14
+ @frame_id = frame_id
15
+ @frame_name = frame_name
16
+ @slot_fillers = slot_fillers.dup
17
+ @context = context
18
+ @confidence = confidence
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def complete?
23
+ filled_count.positive?
24
+ end
25
+
26
+ def filled_count
27
+ @slot_fillers.count { |_k, v| !v.nil? }
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ id: @id,
33
+ frame_id: @frame_id,
34
+ frame_name: @frame_name,
35
+ slot_fillers: @slot_fillers,
36
+ context: @context,
37
+ confidence: @confidence,
38
+ filled_count: filled_count,
39
+ complete: complete?,
40
+ created_at: @created_at
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FrameSemantics
6
+ module Runners
7
+ module FrameSemantics
8
+ def create_semantic_frame(name:, domain:, **)
9
+ frame = engine.create_frame(name: name, domain: domain)
10
+ Legion::Logging.debug "[frame_semantics] created frame name=#{name} domain=#{domain} id=#{frame.id[0..7]}"
11
+ { frame_id: frame.id, name: frame.name, domain: frame.domain, created: true }
12
+ end
13
+
14
+ def define_frame_slot(frame_id:, name:, slot_type: :core, required: true, **)
15
+ frame = engine.define_slot(frame_id: frame_id, name: name, slot_type: slot_type, required: required)
16
+ if frame
17
+ Legion::Logging.debug "[frame_semantics] defined slot #{name} on frame #{frame_id[0..7]}"
18
+ { defined: true, frame_id: frame_id, slot_name: name, slot_type: slot_type }
19
+ else
20
+ { defined: false, reason: :frame_not_found }
21
+ end
22
+ end
23
+
24
+ def fill_frame_slot(frame_id:, slot_name:, filler:, **)
25
+ result = engine.fill_slot(frame_id: frame_id, slot_name: slot_name, filler: filler)
26
+ if result
27
+ Legion::Logging.debug "[frame_semantics] filled slot #{slot_name} on frame #{frame_id[0..7]}"
28
+ { filled: true, frame_id: frame_id, slot_name: slot_name, filler: filler }
29
+ else
30
+ { filled: false, reason: :slot_or_frame_not_found }
31
+ end
32
+ end
33
+
34
+ def instantiate_semantic_frame(frame_id:, context:, confidence: 0.7, **)
35
+ instance = engine.instantiate_frame(frame_id: frame_id, context: context, confidence: confidence)
36
+ if instance
37
+ Legion::Logging.debug "[frame_semantics] instantiated frame #{frame_id[0..7]} instance=#{instance.id[0..7]}"
38
+ { instantiated: true, instance_id: instance.id, frame_id: frame_id, filled_count: instance.filled_count }
39
+ else
40
+ { instantiated: false, reason: :frame_not_found }
41
+ end
42
+ end
43
+
44
+ def add_frame_relation(frame_id:, relation:, target_frame_id:, **)
45
+ result = engine.add_frame_relation(frame_id: frame_id, relation: relation, target_frame_id: target_frame_id)
46
+ if result
47
+ Legion::Logging.debug "[frame_semantics] added relation #{relation} #{frame_id[0..7]}->#{target_frame_id[0..7]}"
48
+ { added: true, frame_id: frame_id, relation: relation, target_frame_id: target_frame_id }
49
+ else
50
+ { added: false, reason: :invalid_frame_or_relation }
51
+ end
52
+ end
53
+
54
+ def activate_semantic_frame(frame_id:, **)
55
+ result = engine.activate_frame(frame_id: frame_id)
56
+ if result
57
+ Legion::Logging.debug "[frame_semantics] activated frame #{frame_id[0..7]}"
58
+ { activated: true, frame_id: frame_id }
59
+ else
60
+ { activated: false, reason: :frame_not_found }
61
+ end
62
+ end
63
+
64
+ def active_frames_report(**)
65
+ frames = engine.active_frames
66
+ Legion::Logging.debug "[frame_semantics] active_frames count=#{frames.size}"
67
+ { frames: frames.map(&:to_h), count: frames.size }
68
+ end
69
+
70
+ def related_frames_report(frame_id:, **)
71
+ frames = engine.related_frames(frame_id: frame_id)
72
+ Legion::Logging.debug "[frame_semantics] related_frames frame=#{frame_id[0..7]} count=#{frames.size}"
73
+ { frames: frames.map(&:to_h), count: frames.size }
74
+ end
75
+
76
+ def complete_frames_report(**)
77
+ frames = engine.complete_frames
78
+ Legion::Logging.debug "[frame_semantics] complete_frames count=#{frames.size}"
79
+ { frames: frames.map(&:to_h), count: frames.size }
80
+ end
81
+
82
+ def update_frame_semantics(**)
83
+ engine.decay_all
84
+ engine.prune_inactive
85
+ Legion::Logging.debug '[frame_semantics] decay_all + prune_inactive complete'
86
+ { updated: true, stats: engine.to_h }
87
+ end
88
+
89
+ def frame_semantics_stats(**)
90
+ engine.to_h
91
+ end
92
+
93
+ private
94
+
95
+ def engine
96
+ @engine ||= Helpers::FrameEngine.new
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FrameSemantics
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/frame_semantics/version'
4
+ require 'legion/extensions/frame_semantics/helpers/constants'
5
+ require 'legion/extensions/frame_semantics/helpers/frame'
6
+ require 'legion/extensions/frame_semantics/helpers/frame_instance'
7
+ require 'legion/extensions/frame_semantics/helpers/frame_engine'
8
+ require 'legion/extensions/frame_semantics/runners/frame_semantics'
9
+ require 'legion/extensions/frame_semantics/helpers/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module FrameSemantics
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::FrameSemantics::Helpers::FrameEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ let(:frame) { engine.create_frame(name: :commercial_transaction, domain: :commerce) }
7
+
8
+ before do
9
+ frame.add_slot(name: :buyer)
10
+ frame.add_slot(name: :seller)
11
+ frame.add_slot(name: :goods)
12
+ end
13
+
14
+ describe '#create_frame' do
15
+ it 'returns a Frame object' do
16
+ f = engine.create_frame(name: :motion, domain: :physics)
17
+ expect(f).to be_a(Legion::Extensions::FrameSemantics::Helpers::Frame)
18
+ end
19
+
20
+ it 'creates frame with slots from hash' do
21
+ f = engine.create_frame(
22
+ name: :competition,
23
+ domain: :sports,
24
+ slots: { winner: { type: :core, required: true }, loser: { type: :peripheral, required: false } }
25
+ )
26
+ expect(f.slots[:winner][:type]).to eq(:core)
27
+ expect(f.slots[:loser][:type]).to eq(:peripheral)
28
+ end
29
+ end
30
+
31
+ describe '#define_slot' do
32
+ it 'adds a slot to an existing frame' do
33
+ result = engine.define_slot(frame_id: frame.id, name: :price)
34
+ expect(result).to be_a(Legion::Extensions::FrameSemantics::Helpers::Frame)
35
+ expect(frame.slots[:price]).not_to be_nil
36
+ end
37
+
38
+ it 'returns nil for unknown frame_id' do
39
+ expect(engine.define_slot(frame_id: 'bogus', name: :price)).to be_nil
40
+ end
41
+ end
42
+
43
+ describe '#fill_slot' do
44
+ it 'fills a slot and returns true' do
45
+ expect(engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')).to be true
46
+ end
47
+
48
+ it 'returns false for unknown frame' do
49
+ expect(engine.fill_slot(frame_id: 'bogus', slot_name: :buyer, filler: 'x')).to be false
50
+ end
51
+
52
+ it 'activates the frame when slot is filled' do
53
+ initial = frame.activation
54
+ engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
55
+ expect(frame.activation_count).to be >= 1
56
+ expect(frame.activation).to be >= initial
57
+ end
58
+ end
59
+
60
+ describe '#instantiate_frame' do
61
+ before do
62
+ engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
63
+ engine.fill_slot(frame_id: frame.id, slot_name: :seller, filler: 'Bob')
64
+ end
65
+
66
+ it 'creates a FrameInstance snapshot' do
67
+ inst = engine.instantiate_frame(frame_id: frame.id, context: 'coffee shop')
68
+ expect(inst).to be_a(Legion::Extensions::FrameSemantics::Helpers::FrameInstance)
69
+ expect(inst.frame_id).to eq(frame.id)
70
+ expect(inst.context).to eq('coffee shop')
71
+ end
72
+
73
+ it 'returns nil for unknown frame' do
74
+ expect(engine.instantiate_frame(frame_id: 'bogus', context: 'x')).to be_nil
75
+ end
76
+
77
+ it 'snapshots current slot fillers' do
78
+ inst = engine.instantiate_frame(frame_id: frame.id, context: 'test')
79
+ expect(inst.slot_fillers[:buyer]).to eq('Alice')
80
+ end
81
+ end
82
+
83
+ describe '#add_frame_relation' do
84
+ it 'adds a valid relation' do
85
+ target = engine.create_frame(name: :transfer, domain: :commerce)
86
+ result = engine.add_frame_relation(
87
+ frame_id: frame.id,
88
+ relation: :has_subframe,
89
+ target_frame_id: target.id
90
+ )
91
+ expect(result).to be true
92
+ expect(frame.relations.size).to eq(1)
93
+ end
94
+
95
+ it 'rejects invalid relation type' do
96
+ result = engine.add_frame_relation(
97
+ frame_id: frame.id,
98
+ relation: :invalid_relation,
99
+ target_frame_id: SecureRandom.uuid
100
+ )
101
+ expect(result).to be false
102
+ end
103
+
104
+ it 'returns false for unknown frame_id' do
105
+ expect(
106
+ engine.add_frame_relation(frame_id: 'bogus', relation: :uses, target_frame_id: SecureRandom.uuid)
107
+ ).to be false
108
+ end
109
+ end
110
+
111
+ describe '#activate_frame' do
112
+ it 'activates an existing frame' do
113
+ initial_count = frame.activation_count
114
+ engine.activate_frame(frame_id: frame.id)
115
+ expect(frame.activation_count).to eq(initial_count + 1)
116
+ end
117
+
118
+ it 'returns false for unknown frame' do
119
+ expect(engine.activate_frame(frame_id: 'bogus')).to be false
120
+ end
121
+ end
122
+
123
+ describe '#active_frames' do
124
+ it 'returns frames with activation > 0.5' do
125
+ 5.times { engine.activate_frame(frame_id: frame.id) }
126
+ expect(engine.active_frames).to include(frame)
127
+ end
128
+
129
+ it 'excludes frames below threshold' do
130
+ new_engine = described_class.new
131
+ f = new_engine.create_frame(name: :cold, domain: :test)
132
+ 100.times { f.decay! }
133
+ expect(new_engine.active_frames).not_to include(f)
134
+ end
135
+ end
136
+
137
+ describe '#frames_by_domain' do
138
+ before do
139
+ engine.create_frame(name: :motion, domain: :physics)
140
+ engine.create_frame(name: :competition, domain: :sports)
141
+ end
142
+
143
+ it 'returns only frames matching the domain' do
144
+ result = engine.frames_by_domain(domain: :physics)
145
+ expect(result.map(&:domain)).to all(eq(:physics))
146
+ end
147
+ end
148
+
149
+ describe '#related_frames' do
150
+ it 'traverses relations to return related frames' do
151
+ target = engine.create_frame(name: :payment, domain: :commerce)
152
+ engine.add_frame_relation(frame_id: frame.id, relation: :has_subframe, target_frame_id: target.id)
153
+ related = engine.related_frames(frame_id: frame.id)
154
+ expect(related).to include(target)
155
+ end
156
+
157
+ it 'returns empty array for unknown frame' do
158
+ expect(engine.related_frames(frame_id: 'bogus')).to eq([])
159
+ end
160
+ end
161
+
162
+ describe '#most_activated' do
163
+ it 'returns top N frames sorted by activation desc' do
164
+ f2 = engine.create_frame(name: :motion, domain: :physics)
165
+ 5.times { engine.activate_frame(frame_id: frame.id) }
166
+ result = engine.most_activated(limit: 2)
167
+ expect(result.first).to eq(frame)
168
+ expect(result).not_to include(f2) if result.size < 2
169
+ end
170
+ end
171
+
172
+ describe '#instances_for_frame' do
173
+ it 'returns all instances of a specific frame' do
174
+ engine.instantiate_frame(frame_id: frame.id, context: 'ctx1')
175
+ engine.instantiate_frame(frame_id: frame.id, context: 'ctx2')
176
+ expect(engine.instances_for_frame(frame_id: frame.id).size).to eq(2)
177
+ end
178
+ end
179
+
180
+ describe '#complete_frames' do
181
+ it 'returns only complete frames' do
182
+ engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
183
+ engine.fill_slot(frame_id: frame.id, slot_name: :seller, filler: 'Bob')
184
+ engine.fill_slot(frame_id: frame.id, slot_name: :goods, filler: 'coffee')
185
+ expect(engine.complete_frames).to include(frame)
186
+ end
187
+
188
+ it 'excludes incomplete frames' do
189
+ engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
190
+ expect(engine.complete_frames).not_to include(frame)
191
+ end
192
+ end
193
+
194
+ describe '#decay_all' do
195
+ it 'decays all frame activations' do
196
+ initial = frame.activation
197
+ engine.decay_all
198
+ expect(frame.activation).to be < initial
199
+ end
200
+ end
201
+
202
+ describe '#prune_inactive' do
203
+ it 'removes frames with activation <= 0.05' do
204
+ f = engine.create_frame(name: :ghost, domain: :test)
205
+ 100.times { f.decay! }
206
+ engine.prune_inactive
207
+ expect(engine.instances_for_frame(frame_id: f.id)).to be_empty
208
+ end
209
+
210
+ it 'retains frames with activation above threshold' do
211
+ 5.times { engine.activate_frame(frame_id: frame.id) }
212
+ engine.prune_inactive
213
+ expect(engine.active_frames).to include(frame)
214
+ end
215
+ end
216
+
217
+ describe '#to_h' do
218
+ it 'returns stats hash with expected keys' do
219
+ h = engine.to_h
220
+ expect(h).to include(:frame_count, :instance_count, :active_count, :complete_count, :domains)
221
+ end
222
+
223
+ it 'reflects current state' do
224
+ expect(engine.to_h[:frame_count]).to eq(1)
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::FrameSemantics::Helpers::FrameInstance do
4
+ let(:frame_id) { SecureRandom.uuid }
5
+ let(:frame_name) { :commercial_transaction }
6
+ let(:slot_fillers) do
7
+ { buyer: 'Alice', seller: 'Bob', goods: nil }
8
+ end
9
+
10
+ subject(:instance) do
11
+ described_class.new(
12
+ frame_id: frame_id,
13
+ frame_name: frame_name,
14
+ slot_fillers: slot_fillers,
15
+ context: 'buying coffee',
16
+ confidence: 0.85
17
+ )
18
+ end
19
+
20
+ describe '#initialize' do
21
+ it 'assigns a uuid id' do
22
+ expect(instance.id).to match(/\A[0-9a-f-]{36}\z/)
23
+ end
24
+
25
+ it 'stores frame metadata' do
26
+ expect(instance.frame_id).to eq(frame_id)
27
+ expect(instance.frame_name).to eq(frame_name)
28
+ end
29
+
30
+ it 'duplicates slot_fillers' do
31
+ inst = instance
32
+ slot_fillers[:buyer] = 'Charlie'
33
+ expect(inst.slot_fillers[:buyer]).to eq('Alice')
34
+ end
35
+
36
+ it 'stores context and confidence' do
37
+ expect(instance.context).to eq('buying coffee')
38
+ expect(instance.confidence).to eq(0.85)
39
+ end
40
+ end
41
+
42
+ describe '#filled_count' do
43
+ it 'counts non-nil fillers' do
44
+ expect(instance.filled_count).to eq(2)
45
+ end
46
+
47
+ it 'returns 0 when all fillers are nil' do
48
+ empty = described_class.new(
49
+ frame_id: frame_id, frame_name: frame_name,
50
+ slot_fillers: { buyer: nil }, context: 'x'
51
+ )
52
+ expect(empty.filled_count).to eq(0)
53
+ end
54
+ end
55
+
56
+ describe '#complete?' do
57
+ it 'returns true when at least one slot is filled' do
58
+ expect(instance.complete?).to be true
59
+ end
60
+
61
+ it 'returns false when all slots are nil' do
62
+ empty = described_class.new(
63
+ frame_id: frame_id, frame_name: frame_name,
64
+ slot_fillers: { buyer: nil }, context: 'x'
65
+ )
66
+ expect(empty.complete?).to be false
67
+ end
68
+ end
69
+
70
+ describe '#to_h' do
71
+ it 'returns expected keys' do
72
+ h = instance.to_h
73
+ expect(h).to include(:id, :frame_id, :frame_name, :slot_fillers,
74
+ :context, :confidence, :filled_count, :complete, :created_at)
75
+ end
76
+
77
+ it 'includes filled_count and complete flag' do
78
+ h = instance.to_h
79
+ expect(h[:filled_count]).to eq(2)
80
+ expect(h[:complete]).to be true
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::FrameSemantics::Helpers::Frame do
4
+ subject(:frame) { described_class.new(name: :commercial_transaction, domain: :commerce) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(frame.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'sets name and domain' do
12
+ expect(frame.name).to eq(:commercial_transaction)
13
+ expect(frame.domain).to eq(:commerce)
14
+ end
15
+
16
+ it 'starts with default activation' do
17
+ expect(frame.activation).to eq(Legion::Extensions::FrameSemantics::Helpers::DEFAULT_ACTIVATION)
18
+ end
19
+
20
+ it 'starts with zero activation_count' do
21
+ expect(frame.activation_count).to eq(0)
22
+ end
23
+
24
+ it 'starts with empty slots and relations' do
25
+ expect(frame.slots).to be_empty
26
+ expect(frame.relations).to be_empty
27
+ end
28
+ end
29
+
30
+ describe '#add_slot' do
31
+ it 'adds a core slot by default' do
32
+ frame.add_slot(name: :buyer)
33
+ expect(frame.slots[:buyer][:type]).to eq(:core)
34
+ expect(frame.slots[:buyer][:required]).to be true
35
+ expect(frame.slots[:buyer][:filler]).to be_nil
36
+ end
37
+
38
+ it 'adds a peripheral slot when specified' do
39
+ frame.add_slot(name: :payment_method, slot_type: :peripheral, required: false)
40
+ expect(frame.slots[:payment_method][:type]).to eq(:peripheral)
41
+ expect(frame.slots[:payment_method][:required]).to be false
42
+ end
43
+
44
+ it 'returns self for chaining' do
45
+ result = frame.add_slot(name: :seller)
46
+ expect(result).to be(frame)
47
+ end
48
+ end
49
+
50
+ describe '#fill_slot' do
51
+ before { frame.add_slot(name: :buyer) }
52
+
53
+ it 'fills a slot and returns true' do
54
+ result = frame.fill_slot(name: :buyer, filler: 'Alice')
55
+ expect(result).to be true
56
+ expect(frame.slots[:buyer][:filler]).to eq('Alice')
57
+ end
58
+
59
+ it 'boosts activation when slot is filled' do
60
+ initial = frame.activation
61
+ frame.fill_slot(name: :buyer, filler: 'Alice')
62
+ expect(frame.activation).to be > initial
63
+ end
64
+
65
+ it 'returns false for unknown slot' do
66
+ expect(frame.fill_slot(name: :unknown, filler: 'x')).to be false
67
+ end
68
+ end
69
+
70
+ describe '#clear_slot' do
71
+ before do
72
+ frame.add_slot(name: :buyer)
73
+ frame.fill_slot(name: :buyer, filler: 'Alice')
74
+ end
75
+
76
+ it 'clears the slot filler' do
77
+ frame.clear_slot(name: :buyer)
78
+ expect(frame.slots[:buyer][:filler]).to be_nil
79
+ end
80
+
81
+ it 'returns true for existing slot' do
82
+ expect(frame.clear_slot(name: :buyer)).to be true
83
+ end
84
+
85
+ it 'returns false for unknown slot' do
86
+ expect(frame.clear_slot(name: :ghost)).to be false
87
+ end
88
+ end
89
+
90
+ describe '#core_slots' do
91
+ before do
92
+ frame.add_slot(name: :buyer, slot_type: :core)
93
+ frame.add_slot(name: :seller, slot_type: :core)
94
+ frame.add_slot(name: :note, slot_type: :peripheral)
95
+ end
96
+
97
+ it 'returns only core slots' do
98
+ expect(frame.core_slots.keys).to contain_exactly(:buyer, :seller)
99
+ end
100
+ end
101
+
102
+ describe '#filled_slots' do
103
+ before do
104
+ frame.add_slot(name: :buyer)
105
+ frame.add_slot(name: :seller)
106
+ frame.fill_slot(name: :buyer, filler: 'Alice')
107
+ end
108
+
109
+ it 'returns only filled slots' do
110
+ expect(frame.filled_slots.keys).to eq([:buyer])
111
+ end
112
+ end
113
+
114
+ describe '#completion_ratio' do
115
+ it 'returns 0.0 with no slots' do
116
+ expect(frame.completion_ratio).to eq(0.0)
117
+ end
118
+
119
+ it 'returns correct fraction of filled core slots' do
120
+ frame.add_slot(name: :buyer)
121
+ frame.add_slot(name: :seller)
122
+ frame.add_slot(name: :goods)
123
+ frame.fill_slot(name: :buyer, filler: 'Alice')
124
+ expect(frame.completion_ratio).to be_within(0.001).of(1.0 / 3.0)
125
+ end
126
+
127
+ it 'ignores peripheral slots in ratio' do
128
+ frame.add_slot(name: :buyer, slot_type: :core)
129
+ frame.add_slot(name: :note, slot_type: :peripheral)
130
+ frame.fill_slot(name: :buyer, filler: 'Alice')
131
+ expect(frame.completion_ratio).to eq(1.0)
132
+ end
133
+ end
134
+
135
+ describe '#complete?' do
136
+ it 'returns false when completion is below threshold' do
137
+ frame.add_slot(name: :buyer)
138
+ frame.add_slot(name: :seller)
139
+ frame.add_slot(name: :goods)
140
+ frame.fill_slot(name: :buyer, filler: 'Alice')
141
+ expect(frame.complete?).to be false
142
+ end
143
+
144
+ it 'returns true when all core slots are filled' do
145
+ frame.add_slot(name: :buyer)
146
+ frame.fill_slot(name: :buyer, filler: 'Alice')
147
+ expect(frame.complete?).to be true
148
+ end
149
+ end
150
+
151
+ describe '#activate!' do
152
+ it 'boosts activation and increments count' do
153
+ initial = frame.activation
154
+ frame.activate!
155
+ expect(frame.activation).to be > initial
156
+ expect(frame.activation_count).to eq(1)
157
+ end
158
+
159
+ it 'clamps activation at 1.0' do
160
+ 10.times { frame.activate! }
161
+ expect(frame.activation).to eq(1.0)
162
+ end
163
+ end
164
+
165
+ describe '#decay!' do
166
+ it 'reduces activation' do
167
+ initial = frame.activation
168
+ frame.decay!
169
+ expect(frame.activation).to be < initial
170
+ end
171
+
172
+ it 'clamps activation at 0.0' do
173
+ 100.times { frame.decay! }
174
+ expect(frame.activation).to eq(0.0)
175
+ end
176
+ end
177
+
178
+ describe '#activation_label' do
179
+ it 'returns :dominant for high activation' do
180
+ 10.times { frame.activate! }
181
+ expect(frame.activation_label).to eq(:dominant)
182
+ end
183
+
184
+ it 'returns :inactive for very low activation' do
185
+ 100.times { frame.decay! }
186
+ expect(frame.activation_label).to eq(:inactive)
187
+ end
188
+
189
+ it 'returns :primed for mid activation around 0.5' do
190
+ frame.instance_variable_set(:@activation, 0.5)
191
+ expect(frame.activation_label).to eq(:primed)
192
+ end
193
+ end
194
+
195
+ describe '#add_relation' do
196
+ it 'appends a relation entry' do
197
+ other_id = SecureRandom.uuid
198
+ frame.add_relation(relation: :inherits_from, target_frame_id: other_id)
199
+ expect(frame.relations.size).to eq(1)
200
+ expect(frame.relations.first[:relation]).to eq(:inherits_from)
201
+ expect(frame.relations.first[:target_frame_id]).to eq(other_id)
202
+ end
203
+ end
204
+
205
+ describe '#to_h' do
206
+ it 'returns a hash with expected keys' do
207
+ h = frame.to_h
208
+ expect(h).to include(:id, :name, :domain, :slots, :relations,
209
+ :activation, :activation_label, :activation_count,
210
+ :completion_ratio, :complete, :created_at)
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/frame_semantics/helpers/client'
4
+
5
+ RSpec.describe Legion::Extensions::FrameSemantics::Runners::FrameSemantics do
6
+ let(:client) { Legion::Extensions::FrameSemantics::Helpers::Client.new }
7
+
8
+ describe '#create_semantic_frame' do
9
+ it 'creates a frame and returns its id' do
10
+ result = client.create_semantic_frame(name: :commercial_transaction, domain: :commerce)
11
+ expect(result[:created]).to be true
12
+ expect(result[:frame_id]).to match(/\A[0-9a-f-]{36}\z/)
13
+ expect(result[:name]).to eq(:commercial_transaction)
14
+ end
15
+ end
16
+
17
+ describe '#define_frame_slot' do
18
+ let(:frame_id) { client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id] }
19
+
20
+ it 'adds a slot to a known frame' do
21
+ result = client.define_frame_slot(frame_id: frame_id, name: :mover)
22
+ expect(result[:defined]).to be true
23
+ expect(result[:slot_name]).to eq(:mover)
24
+ end
25
+
26
+ it 'returns not found for unknown frame' do
27
+ result = client.define_frame_slot(frame_id: 'bogus', name: :mover)
28
+ expect(result[:defined]).to be false
29
+ expect(result[:reason]).to eq(:frame_not_found)
30
+ end
31
+ end
32
+
33
+ describe '#fill_frame_slot' do
34
+ let(:frame_id) do
35
+ id = client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id]
36
+ client.define_frame_slot(frame_id: id, name: :mover)
37
+ id
38
+ end
39
+
40
+ it 'fills an existing slot' do
41
+ result = client.fill_frame_slot(frame_id: frame_id, slot_name: :mover, filler: 'the car')
42
+ expect(result[:filled]).to be true
43
+ expect(result[:filler]).to eq('the car')
44
+ end
45
+
46
+ it 'returns error for unknown slot' do
47
+ result = client.fill_frame_slot(frame_id: frame_id, slot_name: :nonexistent, filler: 'x')
48
+ expect(result[:filled]).to be false
49
+ end
50
+ end
51
+
52
+ describe '#instantiate_semantic_frame' do
53
+ let(:frame_id) do
54
+ id = client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id]
55
+ client.define_frame_slot(frame_id: id, name: :mover)
56
+ client.fill_frame_slot(frame_id: id, slot_name: :mover, filler: 'rocket')
57
+ id
58
+ end
59
+
60
+ it 'creates an instance snapshot' do
61
+ result = client.instantiate_semantic_frame(frame_id: frame_id, context: 'test context')
62
+ expect(result[:instantiated]).to be true
63
+ expect(result[:instance_id]).to match(/\A[0-9a-f-]{36}\z/)
64
+ expect(result[:filled_count]).to eq(1)
65
+ end
66
+
67
+ it 'returns error for unknown frame' do
68
+ result = client.instantiate_semantic_frame(frame_id: 'bogus', context: 'x')
69
+ expect(result[:instantiated]).to be false
70
+ end
71
+ end
72
+
73
+ describe '#add_frame_relation' do
74
+ let(:frame_id) { client.create_semantic_frame(name: :transfer, domain: :commerce)[:frame_id] }
75
+ let(:target_id) { client.create_semantic_frame(name: :payment, domain: :commerce)[:frame_id] }
76
+
77
+ it 'adds a valid frame relation' do
78
+ result = client.add_frame_relation(
79
+ frame_id: frame_id, relation: :has_subframe, target_frame_id: target_id
80
+ )
81
+ expect(result[:added]).to be true
82
+ end
83
+
84
+ it 'rejects an invalid relation' do
85
+ result = client.add_frame_relation(
86
+ frame_id: frame_id, relation: :nonsense, target_frame_id: target_id
87
+ )
88
+ expect(result[:added]).to be false
89
+ end
90
+ end
91
+
92
+ describe '#activate_semantic_frame' do
93
+ let(:frame_id) { client.create_semantic_frame(name: :competition, domain: :sports)[:frame_id] }
94
+
95
+ it 'activates the frame' do
96
+ result = client.activate_semantic_frame(frame_id: frame_id)
97
+ expect(result[:activated]).to be true
98
+ end
99
+
100
+ it 'returns error for unknown frame' do
101
+ result = client.activate_semantic_frame(frame_id: 'bogus')
102
+ expect(result[:activated]).to be false
103
+ end
104
+ end
105
+
106
+ describe '#active_frames_report' do
107
+ it 'returns list of active frames' do
108
+ id = client.create_semantic_frame(name: :competition, domain: :sports)[:frame_id]
109
+ 4.times { client.activate_semantic_frame(frame_id: id) }
110
+ result = client.active_frames_report
111
+ expect(result).to have_key(:frames)
112
+ expect(result).to have_key(:count)
113
+ end
114
+ end
115
+
116
+ describe '#related_frames_report' do
117
+ let(:frame_id) { client.create_semantic_frame(name: :buying, domain: :commerce)[:frame_id] }
118
+ let(:target_id) { client.create_semantic_frame(name: :paying, domain: :commerce)[:frame_id] }
119
+
120
+ before { client.add_frame_relation(frame_id: frame_id, relation: :has_subframe, target_frame_id: target_id) }
121
+
122
+ it 'returns related frames' do
123
+ result = client.related_frames_report(frame_id: frame_id)
124
+ expect(result[:count]).to eq(1)
125
+ expect(result[:frames].first[:id]).to eq(target_id)
126
+ end
127
+ end
128
+
129
+ describe '#complete_frames_report' do
130
+ it 'returns complete frames' do
131
+ id = client.create_semantic_frame(name: :simple, domain: :test)[:frame_id]
132
+ client.define_frame_slot(frame_id: id, name: :actor)
133
+ client.fill_frame_slot(frame_id: id, slot_name: :actor, filler: 'someone')
134
+ result = client.complete_frames_report
135
+ expect(result[:count]).to be >= 1
136
+ end
137
+ end
138
+
139
+ describe '#update_frame_semantics' do
140
+ it 'runs decay and prune, returning updated status' do
141
+ result = client.update_frame_semantics
142
+ expect(result[:updated]).to be true
143
+ expect(result[:stats]).to have_key(:frame_count)
144
+ end
145
+ end
146
+
147
+ describe '#frame_semantics_stats' do
148
+ it 'returns engine stats' do
149
+ client.create_semantic_frame(name: :motion, domain: :physics)
150
+ result = client.frame_semantics_stats
151
+ expect(result[:frame_count]).to be >= 1
152
+ expect(result).to have_key(:domains)
153
+ end
154
+ end
155
+ 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/frame_semantics'
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,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-frame-semantics
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: Fillmore's Frame Semantics engine for LegionIO — conceptual frame activation,
27
+ slot filling, and instance creation
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-frame-semantics.gemspec
36
+ - lib/legion/extensions/frame_semantics.rb
37
+ - lib/legion/extensions/frame_semantics/helpers/client.rb
38
+ - lib/legion/extensions/frame_semantics/helpers/constants.rb
39
+ - lib/legion/extensions/frame_semantics/helpers/frame.rb
40
+ - lib/legion/extensions/frame_semantics/helpers/frame_engine.rb
41
+ - lib/legion/extensions/frame_semantics/helpers/frame_instance.rb
42
+ - lib/legion/extensions/frame_semantics/runners/frame_semantics.rb
43
+ - lib/legion/extensions/frame_semantics/version.rb
44
+ - spec/legion/extensions/frame_semantics/helpers/frame_engine_spec.rb
45
+ - spec/legion/extensions/frame_semantics/helpers/frame_instance_spec.rb
46
+ - spec/legion/extensions/frame_semantics/helpers/frame_spec.rb
47
+ - spec/legion/extensions/frame_semantics/runners/frame_semantics_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-frame-semantics
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-frame-semantics
54
+ source_code_uri: https://github.com/LegionIO/lex-frame-semantics
55
+ documentation_uri: https://github.com/LegionIO/lex-frame-semantics
56
+ changelog_uri: https://github.com/LegionIO/lex-frame-semantics
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-frame-semantics/issues
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.4'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.9
74
+ specification_version: 4
75
+ summary: LEX Frame Semantics
76
+ test_files: []