lex-consent 0.2.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: 8cf5e07bea7eb65742f83c8a32000eb93f0d90b2b35334f7d656ddd71d52824c
4
+ data.tar.gz: c23df3ed550f404a1dbfa6a811bff1bf96ac5bf09567d81ac9c9f161a20fbc8a
5
+ SHA512:
6
+ metadata.gz: 8448d8366e74202edf6755d830e09e729cc038f32394fe88046f552802a4628b9f53323c98af2b5fbd8739522500293efd720c0090cd0939d25ab0e57273f09b
7
+ data.tar.gz: 46da76635d0c2c55d4a62a80ec28dd417bac195f0ec9cd44603a6accf193c7f2966f469097537ee1b77955483190367562da123ff93990dd23b17efedc9edd00
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
+
10
+ gem 'sequel', '>= 5.70'
11
+ gem 'sqlite3', '>= 2.0'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/consent/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-consent'
7
+ spec.version = Legion::Extensions::Consent::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Consent'
12
+ spec.description = 'Four-tier consent gradient with earned autonomy for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-consent'
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-consent'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-consent'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-consent'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-consent/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-consent.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'sequel', '>= 5.70'
29
+ spec.add_development_dependency 'sqlite3', '>= 2.0'
30
+ 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 Consent
8
+ module Actor
9
+ class TierEvaluation < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Consent::Runners::Consent
12
+ end
13
+
14
+ def runner_function
15
+ 'evaluate_and_apply_tiers'
16
+ end
17
+
18
+ def time
19
+ 3600
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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/consent/helpers/tiers'
4
+ require 'legion/extensions/consent/helpers/consent_map'
5
+ require 'legion/extensions/consent/runners/consent'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Consent
10
+ class Client
11
+ include Runners::Consent
12
+
13
+ def initialize(**)
14
+ @consent_map = Helpers::ConsentMap.new
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :consent_map
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Consent
8
+ module Helpers
9
+ class ConsentMap
10
+ APPROVAL_TIMEOUT = 259_200 # 72 hours
11
+
12
+ attr_reader :domains
13
+
14
+ def initialize
15
+ @domains = Hash.new do |h, k|
16
+ h[k] = {
17
+ tier: Tiers::DEFAULT_TIER,
18
+ success_count: 0,
19
+ failure_count: 0,
20
+ total_actions: 0,
21
+ last_changed_at: nil,
22
+ history: [],
23
+ pending_tier: nil,
24
+ pending_since: nil,
25
+ pending_requested_by: nil
26
+ }
27
+ end
28
+ load_from_local
29
+ end
30
+
31
+ def get_tier(domain)
32
+ @domains[domain][:tier]
33
+ end
34
+
35
+ def set_tier(domain, tier)
36
+ return unless Tiers.valid_tier?(tier)
37
+
38
+ entry = @domains[domain]
39
+ old_tier = entry[:tier]
40
+ entry[:tier] = tier
41
+ entry[:last_changed_at] = Time.now.utc
42
+ entry[:history] << { from: old_tier, to: tier, at: Time.now.utc }
43
+ entry[:history].shift while entry[:history].size > 50
44
+ end
45
+
46
+ def record_outcome(domain, success:)
47
+ entry = @domains[domain]
48
+ entry[:total_actions] += 1
49
+ if success
50
+ entry[:success_count] += 1
51
+ else
52
+ entry[:failure_count] += 1
53
+ end
54
+ end
55
+
56
+ def success_rate(domain)
57
+ entry = @domains[domain]
58
+ return 0.0 if entry[:total_actions].zero?
59
+
60
+ entry[:success_count].to_f / entry[:total_actions]
61
+ end
62
+
63
+ def eligible_for_change?(domain)
64
+ entry = @domains[domain]
65
+ return false if entry[:total_actions] < Tiers::MIN_ACTIONS_TO_PROMOTE
66
+
67
+ if entry[:last_changed_at]
68
+ (Time.now.utc - entry[:last_changed_at]) >= Tiers::PROMOTION_COOLDOWN
69
+ else
70
+ true
71
+ end
72
+ end
73
+
74
+ def evaluate_promotion(domain)
75
+ return :ineligible unless eligible_for_change?(domain)
76
+
77
+ rate = success_rate(domain)
78
+ current = get_tier(domain)
79
+
80
+ if rate >= Tiers::PROMOTION_THRESHOLD
81
+ promoted = Tiers.promote(current)
82
+ return :already_max if promoted == current
83
+
84
+ :promote
85
+ elsif rate < Tiers::DEMOTION_THRESHOLD
86
+ demoted = Tiers.demote(current)
87
+ return :already_min if demoted == current
88
+
89
+ :demote
90
+ else
91
+ :maintain
92
+ end
93
+ end
94
+
95
+ def domain_count
96
+ @domains.size
97
+ end
98
+
99
+ def to_h
100
+ @domains.transform_values do |v|
101
+ { tier: v[:tier], success_rate: success_rate_from(v), total_actions: v[:total_actions],
102
+ pending_tier: v[:pending_tier], pending_since: v[:pending_since] }
103
+ end
104
+ end
105
+
106
+ def set_pending(domain, proposed_tier:, requested_by: 'system')
107
+ entry = @domains[domain]
108
+ entry[:pending_tier] = proposed_tier
109
+ entry[:pending_since] = Time.now
110
+ entry[:pending_requested_by] = requested_by
111
+ entry
112
+ end
113
+
114
+ def clear_pending(domain)
115
+ entry = @domains[domain]
116
+ entry[:pending_tier] = nil
117
+ entry[:pending_since] = nil
118
+ entry[:pending_requested_by] = nil
119
+ entry
120
+ end
121
+
122
+ def pending?(domain)
123
+ !@domains[domain][:pending_tier].nil?
124
+ end
125
+
126
+ def pending_expired?(domain, timeout: APPROVAL_TIMEOUT)
127
+ entry = @domains[domain]
128
+ return false unless entry[:pending_since]
129
+
130
+ Time.now - entry[:pending_since] > timeout
131
+ end
132
+
133
+ def save_to_local
134
+ return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
135
+
136
+ dataset = Legion::Data::Local.connection[:consent_domains]
137
+ @domains.each do |domain_key, entry|
138
+ row = {
139
+ domain_key: domain_key,
140
+ tier: entry[:tier].to_s,
141
+ success_count: entry[:success_count],
142
+ failure_count: entry[:failure_count],
143
+ total_actions: entry[:total_actions],
144
+ last_changed_at: entry[:last_changed_at],
145
+ history: ::JSON.generate(entry[:history].map { |h| h.transform_values(&:to_s) })
146
+ }
147
+ existing = dataset.where(domain_key: domain_key).first
148
+ if existing
149
+ dataset.where(domain_key: domain_key).update(row.except(:domain_key))
150
+ else
151
+ dataset.insert(row)
152
+ end
153
+ end
154
+ rescue StandardError => e
155
+ Legion::Logging.warn "[consent] save_to_local failed: #{e.message}" if defined?(Legion::Logging)
156
+ end
157
+
158
+ def load_from_local
159
+ return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
160
+
161
+ Legion::Data::Local.connection[:consent_domains].each do |row|
162
+ key = row[:domain_key]
163
+ history = begin
164
+ ::JSON.parse(row[:history] || '[]', symbolize_names: false).map do |h|
165
+ { from: h['from'].to_sym, to: h['to'].to_sym, at: h['at'] }
166
+ end
167
+ rescue StandardError
168
+ []
169
+ end
170
+
171
+ @domains[key] = {
172
+ tier: row[:tier].to_sym,
173
+ success_count: row[:success_count].to_i,
174
+ failure_count: row[:failure_count].to_i,
175
+ total_actions: row[:total_actions].to_i,
176
+ last_changed_at: row[:last_changed_at],
177
+ history: history
178
+ }
179
+ end
180
+ rescue StandardError => e
181
+ Legion::Logging.warn "[consent] load_from_local failed: #{e.message}" if defined?(Legion::Logging)
182
+ end
183
+
184
+ private
185
+
186
+ def success_rate_from(entry)
187
+ return 0.0 if entry[:total_actions].zero?
188
+
189
+ entry[:success_count].to_f / entry[:total_actions]
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Consent
6
+ module Helpers
7
+ module Tiers
8
+ # Four consent tiers (spec: consent-gradient-spec.md)
9
+ TIERS = %i[autonomous act_notify consult human_only].freeze
10
+
11
+ # Default starting tier for new domains
12
+ DEFAULT_TIER = :consult
13
+
14
+ # Thresholds for tier promotion/demotion
15
+ PROMOTION_THRESHOLD = 0.8 # success rate needed to promote
16
+ DEMOTION_THRESHOLD = 0.5 # success rate below which demotion occurs
17
+ MIN_ACTIONS_TO_PROMOTE = 10 # minimum actions before tier change
18
+ PROMOTION_COOLDOWN = 86_400 # seconds between tier changes (24h)
19
+
20
+ # Tier ordering (lower index = more autonomy)
21
+ TIER_ORDER = { autonomous: 0, act_notify: 1, consult: 2, human_only: 3 }.freeze
22
+
23
+ module_function
24
+
25
+ def valid_tier?(tier)
26
+ TIERS.include?(tier)
27
+ end
28
+
29
+ def more_autonomous?(tier_a, tier_b)
30
+ TIER_ORDER.fetch(tier_a, 99) < TIER_ORDER.fetch(tier_b, 99)
31
+ end
32
+
33
+ def promote(current_tier)
34
+ idx = TIER_ORDER.fetch(current_tier, 2)
35
+ return current_tier if idx.zero?
36
+
37
+ TIER_ORDER.key(idx - 1) || current_tier
38
+ end
39
+
40
+ def demote(current_tier)
41
+ idx = TIER_ORDER.fetch(current_tier, 2)
42
+ return current_tier if idx >= 3
43
+
44
+ TIER_ORDER.key(idx + 1) || current_tier
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:consent_domains) do
6
+ primary_key :id
7
+ String :domain_key, null: false, unique: true, index: true
8
+ String :tier, null: false, default: 'consult'
9
+ Integer :success_count, default: 0
10
+ Integer :failure_count, default: 0
11
+ Integer :total_actions, default: 0
12
+ DateTime :last_changed_at
13
+ String :history, text: true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Consent
6
+ module Runners
7
+ module Consent
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def check_consent(domain:, _action_type: :general, **)
12
+ tier = consent_map.get_tier(domain)
13
+ Legion::Logging.debug "[consent] check: domain=#{domain} tier=#{tier} allowed=#{tier == :autonomous}"
14
+
15
+ {
16
+ domain: domain,
17
+ tier: tier,
18
+ allowed: tier == :autonomous,
19
+ needs_notify: tier == :act_notify,
20
+ needs_consult: tier == :consult,
21
+ human_only: tier == :human_only
22
+ }
23
+ end
24
+
25
+ def record_action(domain:, success:, **)
26
+ consent_map.record_outcome(domain, success: success)
27
+ rate = consent_map.success_rate(domain)
28
+ total = consent_map.domains[domain][:total_actions]
29
+ Legion::Logging.info "[consent] action recorded: domain=#{domain} success=#{success} rate=#{rate.round(2)} total=#{total}"
30
+
31
+ {
32
+ domain: domain,
33
+ success: success,
34
+ success_rate: rate,
35
+ total: total
36
+ }
37
+ end
38
+
39
+ def evaluate_tier_change(domain:, **)
40
+ recommendation = consent_map.evaluate_promotion(domain)
41
+ current = consent_map.get_tier(domain)
42
+
43
+ result = {
44
+ domain: domain,
45
+ current_tier: current,
46
+ recommendation: recommendation,
47
+ success_rate: consent_map.success_rate(domain)
48
+ }
49
+
50
+ case recommendation
51
+ when :promote
52
+ result[:proposed_tier] = Helpers::Tiers.promote(current)
53
+ Legion::Logging.info "[consent] tier change: domain=#{domain} recommend=promote from=#{current} to=#{result[:proposed_tier]}"
54
+ when :demote
55
+ result[:proposed_tier] = Helpers::Tiers.demote(current)
56
+ Legion::Logging.warn "[consent] tier change: domain=#{domain} recommend=demote from=#{current} to=#{result[:proposed_tier]}"
57
+ else
58
+ Legion::Logging.debug "[consent] tier eval: domain=#{domain} current=#{current} recommendation=#{recommendation}"
59
+ end
60
+
61
+ result
62
+ end
63
+
64
+ def apply_tier_change(domain:, new_tier:, **)
65
+ return { error: :invalid_tier, valid_tiers: Helpers::Tiers::TIERS } unless Helpers::Tiers.valid_tier?(new_tier)
66
+
67
+ old_tier = consent_map.get_tier(domain)
68
+ consent_map.set_tier(domain, new_tier)
69
+ changed = old_tier != new_tier
70
+ Legion::Logging.info "[consent] tier applied: domain=#{domain} old=#{old_tier} new=#{new_tier} changed=#{changed}"
71
+ { domain: domain, old_tier: old_tier, new_tier: new_tier, changed: changed }
72
+ end
73
+
74
+ def evaluate_all_tiers(**)
75
+ promotions = []
76
+ demotions = []
77
+
78
+ consent_map.domains.each_key do |domain|
79
+ result = consent_map.evaluate_promotion(domain)
80
+ promotions << domain if result == :promote
81
+ demotions << domain if result == :demote
82
+ end
83
+
84
+ evaluated = consent_map.domain_count
85
+ Legion::Logging.debug "[consent] tier evaluation sweep: domains=#{evaluated} " \
86
+ "promotions=#{promotions.size} demotions=#{demotions.size}"
87
+
88
+ { evaluated: evaluated, promotions: promotions, demotions: demotions }
89
+ end
90
+
91
+ def request_autonomous_approval(domain:, **)
92
+ map = consent_map
93
+ current_tier = map.get_tier(domain)
94
+
95
+ return { requested: false, error: 'already_autonomous' } if current_tier == :autonomous
96
+ return { requested: false, error: 'already_pending' } if map.pending?(domain)
97
+
98
+ map.set_pending(domain, proposed_tier: :autonomous, requested_by: 'tier_evaluation')
99
+
100
+ if defined?(Legion::Events)
101
+ rate = map.success_rate(domain)
102
+ total = map.domains[domain][:total_actions]
103
+ Legion::Events.emit('consent.approval_required', {
104
+ domain: domain,
105
+ current_tier: current_tier,
106
+ proposed_tier: :autonomous,
107
+ success_rate: rate,
108
+ total_actions: total,
109
+ requested_at: Time.now.utc
110
+ })
111
+ end
112
+
113
+ { requested: true, domain: domain, current_tier: current_tier, proposed_tier: :autonomous }
114
+ end
115
+
116
+ def approve_promotion(domain:, approved_by:, **)
117
+ map = consent_map
118
+ return { approved: false, error: 'no_pending_approval' } unless map.pending?(domain)
119
+
120
+ pending_tier = map.domains[domain][:pending_tier]
121
+ old_tier = map.get_tier(domain)
122
+ map.clear_pending(domain)
123
+ map.set_tier(domain, pending_tier)
124
+
125
+ if defined?(Legion::Events)
126
+ Legion::Events.emit('consent.promotion_approved', {
127
+ domain: domain, old_tier: old_tier, new_tier: pending_tier,
128
+ approved_by: approved_by, at: Time.now.utc
129
+ })
130
+ end
131
+
132
+ { approved: true, domain: domain, old_tier: old_tier, new_tier: pending_tier, approved_by: approved_by }
133
+ end
134
+
135
+ def reject_promotion(domain:, rejected_by:, reason: nil, **)
136
+ map = consent_map
137
+ return { rejected: false, error: 'no_pending_approval' } unless map.pending?(domain)
138
+
139
+ map.clear_pending(domain)
140
+
141
+ if defined?(Legion::Events)
142
+ Legion::Events.emit('consent.promotion_rejected', {
143
+ domain: domain, rejected_by: rejected_by, reason: reason, at: Time.now.utc
144
+ })
145
+ end
146
+
147
+ { rejected: true, domain: domain, rejected_by: rejected_by, reason: reason }
148
+ end
149
+
150
+ def expire_pending_approvals(timeout: Helpers::ConsentMap::APPROVAL_TIMEOUT, **)
151
+ map = consent_map
152
+ expired = 0
153
+
154
+ map.domains.each_key do |domain|
155
+ next unless map.pending?(domain)
156
+ next unless map.pending_expired?(domain, timeout: timeout)
157
+
158
+ map.clear_pending(domain)
159
+ expired += 1
160
+ end
161
+
162
+ { expired: expired }
163
+ end
164
+
165
+ def evaluate_and_apply_tiers(**)
166
+ candidates = evaluate_all_tiers
167
+ applied_promotions = 0
168
+ applied_demotions = 0
169
+ approval_requests = 0
170
+
171
+ Array(candidates[:promotions]).each do |domain|
172
+ current = consent_map.get_tier(domain)
173
+ proposed = Helpers::Tiers.promote(current)
174
+
175
+ if proposed == :autonomous
176
+ result = request_autonomous_approval(domain: domain)
177
+ approval_requests += 1 if result[:requested]
178
+ else
179
+ apply_tier_change(domain: domain, new_tier: proposed)
180
+ applied_promotions += 1
181
+ end
182
+ end
183
+
184
+ Array(candidates[:demotions]).each do |domain|
185
+ current = consent_map.get_tier(domain)
186
+ proposed = Helpers::Tiers.demote(current)
187
+ apply_tier_change(domain: domain, new_tier: proposed)
188
+ applied_demotions += 1
189
+ end
190
+
191
+ expired = expire_pending_approvals
192
+
193
+ { evaluated: candidates[:evaluated], applied_promotions: applied_promotions,
194
+ applied_demotions: applied_demotions, approval_requests: approval_requests,
195
+ expired_approvals: expired[:expired] }
196
+ end
197
+
198
+ def consent_status(domain: nil, **)
199
+ if domain
200
+ entry = consent_map.domains[domain]
201
+ Legion::Logging.debug "[consent] status: domain=#{domain} tier=#{entry[:tier]} total=#{entry[:total_actions]}"
202
+ {
203
+ domain: domain,
204
+ tier: entry[:tier],
205
+ success_rate: consent_map.success_rate(domain),
206
+ total: entry[:total_actions],
207
+ eligible: consent_map.eligible_for_change?(domain)
208
+ }
209
+ else
210
+ Legion::Logging.debug "[consent] status: domains=#{consent_map.domain_count}"
211
+ { domains: consent_map.to_h, count: consent_map.domain_count }
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ def consent_map
218
+ @consent_map ||= Helpers::ConsentMap.new
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Consent
6
+ VERSION = '0.2.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/consent/version'
4
+ require 'legion/extensions/consent/helpers/tiers'
5
+ require 'legion/extensions/consent/helpers/consent_map'
6
+ require 'legion/extensions/consent/runners/consent'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Consent
11
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
12
+ end
13
+ end
14
+ end
15
+
16
+ if defined?(Legion::Data::Local)
17
+ Legion::Data::Local.register_migrations(
18
+ name: :consent,
19
+ path: File.join(__dir__, 'consent', 'local_migrations')
20
+ )
21
+ 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/consent/actors/tier_evaluation'
15
+
16
+ RSpec.describe Legion::Extensions::Consent::Actor::TierEvaluation do
17
+ subject(:actor) { described_class.new }
18
+
19
+ describe '#runner_class' do
20
+ it { expect(actor.runner_class).to eq Legion::Extensions::Consent::Runners::Consent }
21
+ end
22
+
23
+ describe '#runner_function' do
24
+ it { expect(actor.runner_function).to eq 'evaluate_and_apply_tiers' }
25
+ end
26
+
27
+ describe '#time' do
28
+ it { expect(actor.time).to eq 3600 }
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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/consent/client'
4
+
5
+ RSpec.describe Legion::Extensions::Consent::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to consent runner methods' do
9
+ expect(client).to respond_to(:check_consent)
10
+ expect(client).to respond_to(:record_action)
11
+ expect(client).to respond_to(:evaluate_tier_change)
12
+ expect(client).to respond_to(:apply_tier_change)
13
+ expect(client).to respond_to(:consent_status)
14
+ end
15
+
16
+ it 'round-trips earned autonomy lifecycle' do
17
+ # Start with consult
18
+ check = client.check_consent(domain: 'scheduling')
19
+ expect(check[:tier]).to eq(:consult)
20
+
21
+ # Build track record
22
+ 12.times { client.record_action(domain: 'scheduling', success: true) }
23
+
24
+ # Evaluate
25
+ eval_result = client.evaluate_tier_change(domain: 'scheduling')
26
+ expect(eval_result[:recommendation]).to eq(:promote)
27
+
28
+ # Apply
29
+ client.apply_tier_change(domain: 'scheduling', new_tier: eval_result[:proposed_tier])
30
+ check = client.check_consent(domain: 'scheduling')
31
+ expect(check[:tier]).to eq(:act_notify)
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Consent::Helpers::Tiers do
4
+ describe '.valid_tier?' do
5
+ it 'accepts valid tiers' do
6
+ %i[autonomous act_notify consult human_only].each do |tier|
7
+ expect(described_class.valid_tier?(tier)).to be true
8
+ end
9
+ end
10
+
11
+ it 'rejects invalid tiers' do
12
+ expect(described_class.valid_tier?(:invalid)).to be false
13
+ end
14
+ end
15
+
16
+ describe '.promote' do
17
+ it 'promotes consult to act_notify' do
18
+ expect(described_class.promote(:consult)).to eq(:act_notify)
19
+ end
20
+
21
+ it 'promotes act_notify to autonomous' do
22
+ expect(described_class.promote(:act_notify)).to eq(:autonomous)
23
+ end
24
+
25
+ it 'cannot promote beyond autonomous' do
26
+ expect(described_class.promote(:autonomous)).to eq(:autonomous)
27
+ end
28
+ end
29
+
30
+ describe '.demote' do
31
+ it 'demotes consult to human_only' do
32
+ expect(described_class.demote(:consult)).to eq(:human_only)
33
+ end
34
+
35
+ it 'cannot demote beyond human_only' do
36
+ expect(described_class.demote(:human_only)).to eq(:human_only)
37
+ end
38
+ end
39
+
40
+ describe '.more_autonomous?' do
41
+ it 'returns true when first tier is more autonomous' do
42
+ expect(described_class.more_autonomous?(:autonomous, :consult)).to be true
43
+ end
44
+
45
+ it 'returns false when second tier is more autonomous' do
46
+ expect(described_class.more_autonomous?(:human_only, :consult)).to be false
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/consent/client'
4
+
5
+ RSpec.describe Legion::Extensions::Consent::Runners::Consent do
6
+ let(:client) { Legion::Extensions::Consent::Client.new }
7
+
8
+ describe '#check_consent' do
9
+ it 'returns default tier for new domain' do
10
+ result = client.check_consent(domain: 'email')
11
+ expect(result[:tier]).to eq(:consult)
12
+ expect(result[:needs_consult]).to be true
13
+ end
14
+ end
15
+
16
+ describe '#record_action' do
17
+ it 'records action outcome' do
18
+ result = client.record_action(domain: 'email', success: true)
19
+ expect(result[:success]).to be true
20
+ expect(result[:total]).to eq(1)
21
+ end
22
+
23
+ it 'tracks success rate' do
24
+ 3.times { client.record_action(domain: 'email', success: true) }
25
+ client.record_action(domain: 'email', success: false)
26
+ result = client.record_action(domain: 'email', success: true)
27
+ expect(result[:success_rate]).to eq(0.8)
28
+ end
29
+ end
30
+
31
+ describe '#evaluate_tier_change' do
32
+ it 'returns ineligible when not enough actions' do
33
+ result = client.evaluate_tier_change(domain: 'email')
34
+ expect(result[:recommendation]).to eq(:ineligible)
35
+ end
36
+
37
+ it 'recommends promotion with high success rate' do
38
+ 15.times { client.record_action(domain: 'calendar', success: true) }
39
+ result = client.evaluate_tier_change(domain: 'calendar')
40
+ expect(result[:recommendation]).to eq(:promote)
41
+ expect(result[:proposed_tier]).to eq(:act_notify)
42
+ end
43
+
44
+ it 'recommends demotion with low success rate' do
45
+ 4.times { client.record_action(domain: 'risky', success: true) }
46
+ 8.times { client.record_action(domain: 'risky', success: false) }
47
+ result = client.evaluate_tier_change(domain: 'risky')
48
+ expect(result[:recommendation]).to eq(:demote)
49
+ expect(result[:proposed_tier]).to eq(:human_only)
50
+ end
51
+ end
52
+
53
+ describe '#apply_tier_change' do
54
+ it 'changes tier' do
55
+ result = client.apply_tier_change(domain: 'email', new_tier: :autonomous)
56
+ expect(result[:new_tier]).to eq(:autonomous)
57
+ expect(result[:changed]).to be true
58
+ end
59
+
60
+ it 'rejects invalid tier' do
61
+ result = client.apply_tier_change(domain: 'email', new_tier: :invalid)
62
+ expect(result[:error]).to eq(:invalid_tier)
63
+ end
64
+ end
65
+
66
+ describe '#evaluate_all_tiers' do
67
+ it 'returns zero evaluated for empty consent map' do
68
+ result = client.evaluate_all_tiers
69
+ expect(result[:evaluated]).to eq(0)
70
+ expect(result[:promotions]).to eq([])
71
+ expect(result[:demotions]).to eq([])
72
+ end
73
+
74
+ it 'includes promoted domains in promotions list' do
75
+ 15.times { client.record_action(domain: 'calendar', success: true) }
76
+ result = client.evaluate_all_tiers
77
+ expect(result[:promotions]).to include('calendar')
78
+ end
79
+
80
+ it 'includes demoted domains in demotions list' do
81
+ 4.times { client.record_action(domain: 'risky', success: true) }
82
+ 8.times { client.record_action(domain: 'risky', success: false) }
83
+ result = client.evaluate_all_tiers
84
+ expect(result[:demotions]).to include('risky')
85
+ end
86
+
87
+ it 'returns evaluated count matching number of known domains' do
88
+ client.record_action(domain: 'email', success: true)
89
+ client.record_action(domain: 'calendar', success: true)
90
+ result = client.evaluate_all_tiers
91
+ expect(result[:evaluated]).to eq(2)
92
+ end
93
+
94
+ it 'does not include ineligible domains in promotions or demotions' do
95
+ client.record_action(domain: 'email', success: true)
96
+ result = client.evaluate_all_tiers
97
+ expect(result[:promotions]).not_to include('email')
98
+ expect(result[:demotions]).not_to include('email')
99
+ end
100
+ end
101
+
102
+ describe '#request_autonomous_approval' do
103
+ it 'sets pending state for non-autonomous domain' do
104
+ client.record_action(domain: 'email', success: true)
105
+ result = client.request_autonomous_approval(domain: 'email')
106
+ expect(result[:requested]).to be true
107
+ expect(result[:proposed_tier]).to eq(:autonomous)
108
+ end
109
+
110
+ it 'rejects if already autonomous' do
111
+ client.apply_tier_change(domain: 'email', new_tier: :autonomous)
112
+ result = client.request_autonomous_approval(domain: 'email')
113
+ expect(result[:requested]).to be false
114
+ expect(result[:error]).to eq('already_autonomous')
115
+ end
116
+
117
+ it 'rejects if already pending' do
118
+ client.request_autonomous_approval(domain: 'email')
119
+ result = client.request_autonomous_approval(domain: 'email')
120
+ expect(result[:requested]).to be false
121
+ expect(result[:error]).to eq('already_pending')
122
+ end
123
+ end
124
+
125
+ describe '#approve_promotion' do
126
+ it 'applies pending tier change' do
127
+ client.request_autonomous_approval(domain: 'email')
128
+ result = client.approve_promotion(domain: 'email', approved_by: 'admin')
129
+ expect(result[:approved]).to be true
130
+ expect(result[:new_tier]).to eq(:autonomous)
131
+ expect(result[:approved_by]).to eq('admin')
132
+ end
133
+
134
+ it 'rejects when no pending approval' do
135
+ result = client.approve_promotion(domain: 'email', approved_by: 'admin')
136
+ expect(result[:approved]).to be false
137
+ expect(result[:error]).to eq('no_pending_approval')
138
+ end
139
+ end
140
+
141
+ describe '#reject_promotion' do
142
+ it 'clears pending state' do
143
+ client.request_autonomous_approval(domain: 'email')
144
+ result = client.reject_promotion(domain: 'email', rejected_by: 'admin', reason: 'not ready')
145
+ expect(result[:rejected]).to be true
146
+ expect(result[:reason]).to eq('not ready')
147
+ end
148
+
149
+ it 'rejects when no pending approval' do
150
+ result = client.reject_promotion(domain: 'email', rejected_by: 'admin')
151
+ expect(result[:rejected]).to be false
152
+ end
153
+ end
154
+
155
+ describe '#expire_pending_approvals' do
156
+ it 'expires stale approvals' do
157
+ client.request_autonomous_approval(domain: 'email')
158
+ result = client.expire_pending_approvals(timeout: 0)
159
+ expect(result[:expired]).to eq(1)
160
+ end
161
+
162
+ it 'does not expire fresh approvals' do
163
+ client.request_autonomous_approval(domain: 'email')
164
+ result = client.expire_pending_approvals(timeout: 999_999)
165
+ expect(result[:expired]).to eq(0)
166
+ end
167
+ end
168
+
169
+ describe '#evaluate_and_apply_tiers' do
170
+ it 'returns summary with zero counts for empty map' do
171
+ result = client.evaluate_and_apply_tiers
172
+ expect(result[:evaluated]).to eq(0)
173
+ expect(result[:applied_promotions]).to eq(0)
174
+ expect(result[:applied_demotions]).to eq(0)
175
+ expect(result[:approval_requests]).to eq(0)
176
+ end
177
+
178
+ it 'auto-applies non-autonomous promotions' do
179
+ 15.times { client.record_action(domain: 'calendar', success: true) }
180
+ result = client.evaluate_and_apply_tiers
181
+ expect(result[:applied_promotions]).to eq(1)
182
+ status = client.consent_status(domain: 'calendar')
183
+ expect(status[:tier]).to eq(:act_notify)
184
+ end
185
+
186
+ it 'requests approval for autonomous promotions instead of auto-applying' do
187
+ client.apply_tier_change(domain: 'calendar', new_tier: :act_notify)
188
+ # Backdate last_changed_at to bypass the 24h cooldown
189
+ map = client.send(:consent_map)
190
+ map.domains['calendar'][:last_changed_at] = Time.now.utc - 100_000
191
+ 15.times { client.record_action(domain: 'calendar', success: true) }
192
+ result = client.evaluate_and_apply_tiers
193
+ expect(result[:approval_requests]).to eq(1)
194
+ expect(result[:applied_promotions]).to eq(0)
195
+ status = client.consent_status(domain: 'calendar')
196
+ expect(status[:tier]).to eq(:act_notify)
197
+ end
198
+
199
+ it 'auto-applies demotions' do
200
+ 4.times { client.record_action(domain: 'risky', success: true) }
201
+ 8.times { client.record_action(domain: 'risky', success: false) }
202
+ result = client.evaluate_and_apply_tiers
203
+ expect(result[:applied_demotions]).to eq(1)
204
+ status = client.consent_status(domain: 'risky')
205
+ expect(status[:tier]).to eq(:human_only)
206
+ end
207
+ end
208
+
209
+ describe '#consent_status' do
210
+ it 'returns domain-specific status' do
211
+ client.record_action(domain: 'email', success: true)
212
+ result = client.consent_status(domain: 'email')
213
+ expect(result[:tier]).to eq(:consult)
214
+ expect(result[:total]).to eq(1)
215
+ end
216
+
217
+ it 'returns all domains when no domain specified' do
218
+ client.record_action(domain: 'email', success: true)
219
+ client.record_action(domain: 'calendar', success: true)
220
+ result = client.consent_status
221
+ expect(result[:count]).to eq(2)
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'sequel'
5
+ require 'json'
6
+ require 'tmpdir'
7
+
8
+ RSpec.describe 'lex-consent local SQLite persistence' do
9
+ let(:db_path) { File.join(Dir.tmpdir, "consent_test_#{Process.pid}_#{rand(9999)}.db") }
10
+ let(:db) { Sequel.sqlite(db_path) }
11
+
12
+ before do
13
+ # Create the schema in the temp DB
14
+ db.create_table(:consent_domains) do
15
+ primary_key :id
16
+ String :domain_key, null: false, unique: true
17
+ String :tier, null: false, default: 'consult'
18
+ Integer :success_count, default: 0
19
+ Integer :failure_count, default: 0
20
+ Integer :total_actions, default: 0
21
+ DateTime :last_changed_at
22
+ String :history, text: true
23
+ end
24
+
25
+ # Stub Legion::Data::Local to use our temp DB
26
+ stub_const('Legion::Data::Local', Module.new do
27
+ def self.connected?
28
+ true
29
+ end
30
+
31
+ def self.connection
32
+ @_connection
33
+ end
34
+
35
+ def self._set_connection(conn)
36
+ @_connection = conn
37
+ end
38
+ end)
39
+
40
+ Legion::Data::Local._set_connection(db)
41
+ end
42
+
43
+ after do
44
+ db.disconnect
45
+ FileUtils.rm_f(db_path)
46
+ end
47
+
48
+ describe 'save_to_local' do
49
+ it 'writes domain state to the database' do
50
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
51
+ map.record_outcome('email', success: true)
52
+ map.record_outcome('email', success: true)
53
+ map.save_to_local
54
+
55
+ row = db[:consent_domains].where(domain_key: 'email').first
56
+ expect(row).not_to be_nil
57
+ expect(row[:domain_key]).to eq('email')
58
+ expect(row[:tier]).to eq('consult')
59
+ expect(row[:success_count]).to eq(2)
60
+ expect(row[:total_actions]).to eq(2)
61
+ end
62
+
63
+ it 'updates an existing row on second save' do
64
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
65
+ map.record_outcome('email', success: true)
66
+ map.save_to_local
67
+
68
+ map.record_outcome('email', success: false)
69
+ map.save_to_local
70
+
71
+ rows = db[:consent_domains].where(domain_key: 'email').all
72
+ expect(rows.size).to eq(1)
73
+ expect(rows.first[:failure_count]).to eq(1)
74
+ expect(rows.first[:total_actions]).to eq(2)
75
+ end
76
+
77
+ it 'serializes tier as a string' do
78
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
79
+ map.set_tier('scheduling', :autonomous)
80
+ map.save_to_local
81
+
82
+ row = db[:consent_domains].where(domain_key: 'scheduling').first
83
+ expect(row[:tier]).to eq('autonomous')
84
+ end
85
+
86
+ it 'serializes history as JSON' do
87
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
88
+ map.set_tier('scheduling', :autonomous)
89
+ map.save_to_local
90
+
91
+ row = db[:consent_domains].where(domain_key: 'scheduling').first
92
+ parsed = JSON.parse(row[:history])
93
+ expect(parsed).to be_an(Array)
94
+ expect(parsed.first['from']).to eq('consult')
95
+ expect(parsed.first['to']).to eq('autonomous')
96
+ end
97
+
98
+ it 'persists multiple domains' do
99
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
100
+ map.record_outcome('email', success: true)
101
+ map.record_outcome('calendar', success: false)
102
+ map.save_to_local
103
+
104
+ expect(db[:consent_domains].count).to eq(2)
105
+ end
106
+ end
107
+
108
+ describe 'load_from_local' do
109
+ it 'restores domain state from the database' do
110
+ db[:consent_domains].insert(
111
+ domain_key: 'email',
112
+ tier: 'act_notify',
113
+ success_count: 5,
114
+ failure_count: 1,
115
+ total_actions: 6,
116
+ history: '[]'
117
+ )
118
+
119
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
120
+ expect(map.get_tier('email')).to eq(:act_notify)
121
+ expect(map.domains['email'][:success_count]).to eq(5)
122
+ expect(map.domains['email'][:total_actions]).to eq(6)
123
+ end
124
+
125
+ it 'restores history as an array of hashes with symbol keys' do
126
+ history_json = JSON.generate([{ 'from' => 'consult', 'to' => 'act_notify', 'at' => Time.now.utc.to_s }])
127
+ db[:consent_domains].insert(
128
+ domain_key: 'scheduling',
129
+ tier: 'act_notify',
130
+ success_count: 10,
131
+ failure_count: 0,
132
+ total_actions: 10,
133
+ history: history_json
134
+ )
135
+
136
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
137
+ history = map.domains['scheduling'][:history]
138
+ expect(history).to be_an(Array)
139
+ expect(history.first[:from]).to eq(:consult)
140
+ expect(history.first[:to]).to eq(:act_notify)
141
+ end
142
+
143
+ it 'handles empty history JSON gracefully' do
144
+ db[:consent_domains].insert(
145
+ domain_key: 'empty_history',
146
+ tier: 'consult',
147
+ success_count: 0,
148
+ failure_count: 0,
149
+ total_actions: 0,
150
+ history: '[]'
151
+ )
152
+
153
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
154
+ expect(map.domains['empty_history'][:history]).to eq([])
155
+ end
156
+ end
157
+
158
+ describe 'round-trip persistence' do
159
+ it 'saves and restores earned tier' do
160
+ # First instance: build history and promote
161
+ map1 = Legion::Extensions::Consent::Helpers::ConsentMap.new
162
+ 15.times { map1.record_outcome('tasks', success: true) }
163
+ map1.set_tier('tasks', :act_notify)
164
+ map1.save_to_local
165
+
166
+ # Second instance: loads from DB
167
+ map2 = Legion::Extensions::Consent::Helpers::ConsentMap.new
168
+ expect(map2.get_tier('tasks')).to eq(:act_notify)
169
+ expect(map2.domains['tasks'][:success_count]).to eq(15)
170
+ expect(map2.domains['tasks'][:total_actions]).to eq(15)
171
+ end
172
+
173
+ it 'round-trips multiple domains independently' do
174
+ map1 = Legion::Extensions::Consent::Helpers::ConsentMap.new
175
+ map1.set_tier('email', :autonomous)
176
+ map1.record_outcome('calendar', success: true)
177
+ map1.record_outcome('calendar', success: false)
178
+ map1.save_to_local
179
+
180
+ map2 = Legion::Extensions::Consent::Helpers::ConsentMap.new
181
+ expect(map2.get_tier('email')).to eq(:autonomous)
182
+ expect(map2.get_tier('calendar')).to eq(:consult)
183
+ expect(map2.domains['calendar'][:success_count]).to eq(1)
184
+ expect(map2.domains['calendar'][:failure_count]).to eq(1)
185
+ end
186
+ end
187
+
188
+ describe 'graceful no-op when Legion::Data::Local is unavailable' do
189
+ before do
190
+ # Simulate no Legion::Data::Local defined
191
+ hide_const('Legion::Data::Local')
192
+ end
193
+
194
+ it 'save_to_local does nothing without raising' do
195
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
196
+ map.record_outcome('email', success: true)
197
+ expect { map.save_to_local }.not_to raise_error
198
+ end
199
+
200
+ it 'load_from_local does nothing without raising (no DB loaded at init)' do
201
+ expect { Legion::Extensions::Consent::Helpers::ConsentMap.new }.not_to raise_error
202
+ end
203
+
204
+ it 'starts with default in-memory state' do
205
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
206
+ expect(map.get_tier('email')).to eq(:consult)
207
+ end
208
+ end
209
+
210
+ describe 'graceful no-op when Legion::Data::Local is defined but not connected' do
211
+ before do
212
+ stub_const('Legion::Data::Local', Module.new do
213
+ def self.connected?
214
+ false
215
+ end
216
+ end)
217
+ end
218
+
219
+ it 'save_to_local does nothing without raising' do
220
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
221
+ map.record_outcome('email', success: true)
222
+ expect { map.save_to_local }.not_to raise_error
223
+ end
224
+
225
+ it 'initialize completes without raising' do
226
+ expect { Legion::Extensions::Consent::Helpers::ConsentMap.new }.not_to raise_error
227
+ end
228
+
229
+ it 'starts with default in-memory state' do
230
+ map = Legion::Extensions::Consent::Helpers::ConsentMap.new
231
+ expect(map.get_tier('email')).to eq(:consult)
232
+ end
233
+ end
234
+ 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/consent'
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,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-consent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.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: sequel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.70'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '5.70'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ description: Four-tier consent gradient with earned autonomy for brain-modeled agentic
41
+ AI
42
+ email:
43
+ - matthewdiverson@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - lex-consent.gemspec
50
+ - lib/legion/extensions/consent.rb
51
+ - lib/legion/extensions/consent/actors/tier_evaluation.rb
52
+ - lib/legion/extensions/consent/client.rb
53
+ - lib/legion/extensions/consent/helpers/consent_map.rb
54
+ - lib/legion/extensions/consent/helpers/tiers.rb
55
+ - lib/legion/extensions/consent/local_migrations/20260316000010_create_consent_domains.rb
56
+ - lib/legion/extensions/consent/runners/consent.rb
57
+ - lib/legion/extensions/consent/version.rb
58
+ - spec/legion/extensions/consent/actors/tier_evaluation_spec.rb
59
+ - spec/legion/extensions/consent/client_spec.rb
60
+ - spec/legion/extensions/consent/helpers/tiers_spec.rb
61
+ - spec/legion/extensions/consent/runners/consent_spec.rb
62
+ - spec/local_persistence_spec.rb
63
+ - spec/spec_helper.rb
64
+ homepage: https://github.com/LegionIO/lex-consent
65
+ licenses:
66
+ - MIT
67
+ metadata:
68
+ homepage_uri: https://github.com/LegionIO/lex-consent
69
+ source_code_uri: https://github.com/LegionIO/lex-consent
70
+ documentation_uri: https://github.com/LegionIO/lex-consent
71
+ changelog_uri: https://github.com/LegionIO/lex-consent
72
+ bug_tracker_uri: https://github.com/LegionIO/lex-consent/issues
73
+ rubygems_mfa_required: 'true'
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.4'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.6.9
89
+ specification_version: 4
90
+ summary: LEX Consent
91
+ test_files: []