lex-temporal-discounting 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: 21a237076ed20a409650645d82f97cc71d6ebb19887c1086292784145ea76d70
4
+ data.tar.gz: f5d63d9514bca73170da0ca88625129e6d5f1d3cb7f40f6bee93b29de14d49d1
5
+ SHA512:
6
+ metadata.gz: a583760854c89b814f44f51e70f63fa1300e36a7ce5c3604865b78fe4721f8209f9a1aded7eeabaaa2606e9262b8b8ad19387daa5a6130d50c61f16be48c9051
7
+ data.tar.gz: b0e1c81d05ce3ad7a101a7be8a2d04ac5dd206927eb77265fed9a573d6a71b220b2c17b443f085d13d112e53fd70ef0b11319406a2561876c16c757f09c514dc
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/temporal_discounting/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-temporal-discounting'
7
+ spec.version = Legion::Extensions::TemporalDiscounting::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Temporal Discounting'
12
+ spec.description = 'Hyperbolic temporal discounting model for brain-modeled agentic AI planning and impulse control'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-temporal-discounting'
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-temporal-discounting'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-temporal-discounting'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-temporal-discounting'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-temporal-discounting/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-temporal-discounting.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/helpers/constants'
4
+ require 'legion/extensions/temporal_discounting/helpers/reward'
5
+ require 'legion/extensions/temporal_discounting/helpers/discounting_engine'
6
+ require 'legion/extensions/temporal_discounting/runners/temporal_discounting'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module TemporalDiscounting
11
+ class Client
12
+ include Runners::TemporalDiscounting
13
+
14
+ def engine
15
+ @engine ||= Helpers::DiscountingEngine.new
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TemporalDiscounting
6
+ module Helpers
7
+ DEFAULT_DISCOUNT_RATE = 0.1
8
+ MIN_DISCOUNT_RATE = 0.01
9
+ MAX_DISCOUNT_RATE = 1.0
10
+ MAX_REWARDS = 500
11
+ DEFAULT_DELAY = 1.0
12
+
13
+ IMPULSIVITY_LABELS = {
14
+ (0.0...0.05) => :patient,
15
+ (0.05...0.15) => :moderate,
16
+ (0.15...0.3) => :impulsive,
17
+ (0.3...0.6) => :very_impulsive,
18
+ (0.6..1.0) => :extreme
19
+ }.freeze
20
+
21
+ VALUE_LABELS = {
22
+ (0.8..) => :full_value,
23
+ (0.6...0.8) => :high_value,
24
+ (0.4...0.6) => :moderate_value,
25
+ (0.2...0.4) => :low_value,
26
+ (..0.2) => :negligible
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TemporalDiscounting
6
+ module Helpers
7
+ class DiscountingEngine
8
+ attr_reader :rewards, :domain_rates
9
+
10
+ def initialize
11
+ @rewards = {}
12
+ @domain_rates = {}
13
+ end
14
+
15
+ def create_reward(label:, amount:, delay:, domain: :general, discount_rate: nil)
16
+ rate = discount_rate || get_domain_rate(domain)
17
+ reward = Reward.new(label: label, amount: amount, delay: delay, domain: domain, discount_rate: rate)
18
+ @rewards[reward.id] = reward
19
+ @rewards.shift while @rewards.size > MAX_REWARDS
20
+ reward
21
+ end
22
+
23
+ def compare_rewards(reward_a_id:, reward_b_id:)
24
+ a = @rewards.fetch(reward_a_id, nil)
25
+ b = @rewards.fetch(reward_b_id, nil)
26
+ return { error: :not_found, missing: missing_ids(reward_a_id, reward_b_id) } if a.nil? || b.nil?
27
+
28
+ delta = (a.subjective_value - b.subjective_value).round(10)
29
+ preferred = if delta.positive?
30
+ reward_a_id
31
+ elsif delta.negative?
32
+ reward_b_id
33
+ else
34
+ :tied
35
+ end
36
+
37
+ {
38
+ preferred: preferred,
39
+ delta: delta.abs,
40
+ reward_a_value: a.subjective_value,
41
+ reward_b_value: b.subjective_value
42
+ }
43
+ end
44
+
45
+ def worth_waiting_for?(reward_id:, threshold: 0.3)
46
+ reward = @rewards.fetch(reward_id, nil)
47
+ return { error: :not_found } if reward.nil?
48
+
49
+ { reward_id: reward_id, worth_waiting: reward.worth_waiting?(threshold: threshold),
50
+ subjective_value: reward.subjective_value, threshold: threshold }
51
+ end
52
+
53
+ def set_domain_rate(domain:, rate:)
54
+ @domain_rates[domain] = rate.clamp(MIN_DISCOUNT_RATE, MAX_DISCOUNT_RATE)
55
+ end
56
+
57
+ def get_domain_rate(domain)
58
+ @domain_rates.fetch(domain, DEFAULT_DISCOUNT_RATE)
59
+ end
60
+
61
+ def immediate_vs_delayed(immediate_amount:, delayed_amount:, delay:, domain: :general)
62
+ k = get_domain_rate(domain)
63
+ delayed_sv = (delayed_amount / (1.0 + (k * delay.to_f))).round(10)
64
+ immediate_sv = immediate_amount.to_f.clamp(0.0, 1.0).round(10)
65
+
66
+ preferred = immediate_sv >= delayed_sv ? :immediate : :delayed
67
+
68
+ {
69
+ preferred: preferred,
70
+ immediate_value: immediate_sv,
71
+ delayed_value: delayed_sv,
72
+ delta: (immediate_sv - delayed_sv).abs.round(10),
73
+ discount_rate: k
74
+ }
75
+ end
76
+
77
+ def optimal_delay(reward_id:, min_value: 0.5)
78
+ reward = @rewards.fetch(reward_id, nil)
79
+ return { error: :not_found } if reward.nil?
80
+
81
+ k = reward.discount_rate
82
+ a = reward.amount
83
+ return { error: :threshold_too_high } if min_value > a
84
+
85
+ max_delay = ((a / min_value) - 1.0) / k
86
+ {
87
+ reward_id: reward_id,
88
+ max_delay: max_delay.round(10),
89
+ min_value: min_value,
90
+ amount: a,
91
+ discount_rate: k
92
+ }
93
+ end
94
+
95
+ def patience_report
96
+ return empty_patience_report if @rewards.empty?
97
+
98
+ values = @rewards.values
99
+ avg_rate = (values.sum(&:discount_rate) / values.size).round(10)
100
+ avg_sv = (values.sum(&:subjective_value) / values.size).round(10)
101
+ distribution = build_impulsivity_distribution(values)
102
+
103
+ {
104
+ total_rewards: @rewards.size,
105
+ avg_discount_rate: avg_rate,
106
+ avg_subjective_value: avg_sv,
107
+ impulsivity_distribution: distribution
108
+ }
109
+ end
110
+
111
+ def rewards_by_domain(domain:)
112
+ @rewards.values.select { |r| r.domain == domain }
113
+ end
114
+
115
+ def most_valuable(limit: 5)
116
+ @rewards.values
117
+ .sort_by { |r| -r.subjective_value }
118
+ .first(limit)
119
+ end
120
+
121
+ def prune_expired(min_value: 0.05)
122
+ before = @rewards.size
123
+ @rewards.reject! { |_, r| r.subjective_value < min_value }
124
+ { pruned: before - @rewards.size, remaining: @rewards.size }
125
+ end
126
+
127
+ def to_h
128
+ {
129
+ total_rewards: @rewards.size,
130
+ domain_rates: @domain_rates,
131
+ patience: patience_report
132
+ }
133
+ end
134
+
135
+ private
136
+
137
+ def missing_ids(id_a, id_b)
138
+ missing = []
139
+ missing << id_a unless @rewards.key?(id_a)
140
+ missing << id_b unless @rewards.key?(id_b)
141
+ missing
142
+ end
143
+
144
+ def empty_patience_report
145
+ {
146
+ total_rewards: 0,
147
+ avg_discount_rate: 0.0,
148
+ avg_subjective_value: 0.0,
149
+ impulsivity_distribution: {}
150
+ }
151
+ end
152
+
153
+ def build_impulsivity_distribution(values)
154
+ distribution = Hash.new(0)
155
+ values.each { |r| distribution[r.impulsivity_label] += 1 }
156
+ distribution
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module TemporalDiscounting
8
+ module Helpers
9
+ class Reward
10
+ attr_reader :id, :label, :amount, :delay, :domain, :discount_rate, :created_at
11
+
12
+ def initialize(label:, amount:, delay:, domain: :general, discount_rate: DEFAULT_DISCOUNT_RATE)
13
+ @id = SecureRandom.uuid
14
+ @label = label
15
+ @amount = amount.clamp(0.0, 1.0)
16
+ @delay = delay.to_f
17
+ @domain = domain
18
+ @discount_rate = discount_rate.clamp(MIN_DISCOUNT_RATE, MAX_DISCOUNT_RATE)
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def subjective_value
23
+ (@amount / (1.0 + (@discount_rate * @delay))).round(10)
24
+ end
25
+
26
+ def value_ratio
27
+ return 0.0 if @amount.zero?
28
+
29
+ (subjective_value / @amount).round(10)
30
+ end
31
+
32
+ def value_label
33
+ ratio = value_ratio
34
+ VALUE_LABELS.each do |range, label|
35
+ return label if range.cover?(ratio)
36
+ end
37
+ :negligible
38
+ end
39
+
40
+ def impulsivity_label
41
+ k = @discount_rate
42
+ IMPULSIVITY_LABELS.each do |range, label|
43
+ return label if range.cover?(k)
44
+ end
45
+ :extreme
46
+ end
47
+
48
+ def worth_waiting?(threshold: 0.3)
49
+ subjective_value >= threshold
50
+ end
51
+
52
+ def adjust_delay!(new_delay:)
53
+ @delay = new_delay.to_f
54
+ end
55
+
56
+ def adjust_discount_rate!(new_rate:)
57
+ @discount_rate = new_rate.clamp(MIN_DISCOUNT_RATE, MAX_DISCOUNT_RATE)
58
+ end
59
+
60
+ def to_h
61
+ {
62
+ id: @id,
63
+ label: @label,
64
+ amount: @amount,
65
+ delay: @delay,
66
+ domain: @domain,
67
+ discount_rate: @discount_rate,
68
+ subjective_value: subjective_value,
69
+ value_ratio: value_ratio,
70
+ value_label: value_label,
71
+ impulsivity_label: impulsivity_label,
72
+ worth_waiting: worth_waiting?,
73
+ created_at: @created_at
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TemporalDiscounting
6
+ module Runners
7
+ module TemporalDiscounting
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_temporal_reward(label:, amount:, delay:, domain: :general, discount_rate: nil, **)
12
+ reward = engine.create_reward(label: label, amount: amount, delay: delay,
13
+ domain: domain, discount_rate: discount_rate)
14
+ Legion::Logging.debug "[temporal_discounting] created reward id=#{reward.id[0..7]} " \
15
+ "sv=#{reward.subjective_value.round(4)} label=#{label}"
16
+ { success: true, reward: reward.to_h }
17
+ rescue StandardError => e
18
+ Legion::Logging.warn "[temporal_discounting] create_temporal_reward failed: #{e.message}"
19
+ { success: false, error: e.message }
20
+ end
21
+
22
+ def compare_temporal_rewards(reward_a_id:, reward_b_id:, **)
23
+ result = engine.compare_rewards(reward_a_id: reward_a_id, reward_b_id: reward_b_id)
24
+ Legion::Logging.debug "[temporal_discounting] compare preferred=#{result[:preferred]}"
25
+ { success: !result.key?(:error), **result }
26
+ rescue StandardError => e
27
+ Legion::Logging.warn "[temporal_discounting] compare_temporal_rewards failed: #{e.message}"
28
+ { success: false, error: e.message }
29
+ end
30
+
31
+ def check_worth_waiting(reward_id:, threshold: 0.3, **)
32
+ result = engine.worth_waiting_for?(reward_id: reward_id, threshold: threshold)
33
+ Legion::Logging.debug "[temporal_discounting] worth_waiting=#{result[:worth_waiting]} id=#{reward_id[0..7]}"
34
+ { success: !result.key?(:error), **result }
35
+ rescue StandardError => e
36
+ Legion::Logging.warn "[temporal_discounting] check_worth_waiting failed: #{e.message}"
37
+ { success: false, error: e.message }
38
+ end
39
+
40
+ def immediate_vs_delayed_comparison(immediate_amount:, delayed_amount:, delay:, domain: :general, **)
41
+ result = engine.immediate_vs_delayed(
42
+ immediate_amount: immediate_amount,
43
+ delayed_amount: delayed_amount,
44
+ delay: delay,
45
+ domain: domain
46
+ )
47
+ Legion::Logging.debug "[temporal_discounting] imm_vs_delayed preferred=#{result[:preferred]}"
48
+ { success: true, **result }
49
+ rescue StandardError => e
50
+ Legion::Logging.warn "[temporal_discounting] immediate_vs_delayed_comparison failed: #{e.message}"
51
+ { success: false, error: e.message }
52
+ end
53
+
54
+ def compute_optimal_delay(reward_id:, min_value: 0.5, **)
55
+ result = engine.optimal_delay(reward_id: reward_id, min_value: min_value)
56
+ Legion::Logging.debug "[temporal_discounting] optimal_delay=#{result[:max_delay]} id=#{reward_id[0..7]}"
57
+ { success: !result.key?(:error), **result }
58
+ rescue StandardError => e
59
+ Legion::Logging.warn "[temporal_discounting] compute_optimal_delay failed: #{e.message}"
60
+ { success: false, error: e.message }
61
+ end
62
+
63
+ def temporal_patience_report(**)
64
+ report = engine.patience_report
65
+ Legion::Logging.debug "[temporal_discounting] patience_report total=#{report[:total_rewards]}"
66
+ { success: true, **report }
67
+ rescue StandardError => e
68
+ Legion::Logging.warn "[temporal_discounting] temporal_patience_report failed: #{e.message}"
69
+ { success: false, error: e.message }
70
+ end
71
+
72
+ def set_domain_discount_rate(domain:, rate:, **)
73
+ engine.set_domain_rate(domain: domain, rate: rate)
74
+ Legion::Logging.debug "[temporal_discounting] set domain=#{domain} rate=#{rate}"
75
+ { success: true, domain: domain, rate: engine.get_domain_rate(domain) }
76
+ rescue StandardError => e
77
+ Legion::Logging.warn "[temporal_discounting] set_domain_discount_rate failed: #{e.message}"
78
+ { success: false, error: e.message }
79
+ end
80
+
81
+ def most_valuable_rewards(limit: 5, **)
82
+ rewards = engine.most_valuable(limit: limit)
83
+ Legion::Logging.debug "[temporal_discounting] most_valuable count=#{rewards.size}"
84
+ { success: true, rewards: rewards.map(&:to_h), count: rewards.size }
85
+ rescue StandardError => e
86
+ Legion::Logging.warn "[temporal_discounting] most_valuable_rewards failed: #{e.message}"
87
+ { success: false, error: e.message }
88
+ end
89
+
90
+ def update_temporal_discounting(min_value: 0.05, **)
91
+ prune_result = engine.prune_expired(min_value: min_value)
92
+ stats = engine.to_h
93
+ Legion::Logging.debug "[temporal_discounting] update pruned=#{prune_result[:pruned]} " \
94
+ "remaining=#{prune_result[:remaining]}"
95
+ { success: true, pruned: prune_result[:pruned], remaining: prune_result[:remaining], stats: stats }
96
+ rescue StandardError => e
97
+ Legion::Logging.warn "[temporal_discounting] update_temporal_discounting failed: #{e.message}"
98
+ { success: false, error: e.message }
99
+ end
100
+
101
+ def temporal_discounting_stats(**)
102
+ stats = engine.to_h
103
+ Legion::Logging.debug "[temporal_discounting] stats total=#{stats[:total_rewards]}"
104
+ { success: true, **stats }
105
+ rescue StandardError => e
106
+ Legion::Logging.warn "[temporal_discounting] temporal_discounting_stats failed: #{e.message}"
107
+ { success: false, error: e.message }
108
+ end
109
+
110
+ private
111
+
112
+ def engine
113
+ @engine ||= Helpers::DiscountingEngine.new
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TemporalDiscounting
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/version'
4
+ require 'legion/extensions/temporal_discounting/helpers/constants'
5
+ require 'legion/extensions/temporal_discounting/helpers/reward'
6
+ require 'legion/extensions/temporal_discounting/helpers/discounting_engine'
7
+ require 'legion/extensions/temporal_discounting/runners/temporal_discounting'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module TemporalDiscounting
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/client'
4
+
5
+ RSpec.describe Legion::Extensions::TemporalDiscounting::Client do
6
+ subject(:client) { described_class.new }
7
+
8
+ it 'includes the TemporalDiscounting runner' do
9
+ expect(client).to respond_to(:create_temporal_reward)
10
+ expect(client).to respond_to(:compare_temporal_rewards)
11
+ expect(client).to respond_to(:temporal_patience_report)
12
+ end
13
+
14
+ it 'exposes the discounting engine' do
15
+ expect(client.engine).to be_a(Legion::Extensions::TemporalDiscounting::Helpers::DiscountingEngine)
16
+ end
17
+
18
+ it 'shares the same engine instance across calls' do
19
+ expect(client.engine).to equal(client.engine)
20
+ end
21
+
22
+ it 'creates different engine instances per client' do
23
+ c2 = described_class.new
24
+ expect(client.engine).not_to equal(c2.engine)
25
+ end
26
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/client'
4
+
5
+ RSpec.describe Legion::Extensions::TemporalDiscounting::Helpers::DiscountingEngine do
6
+ subject(:engine) { described_class.new }
7
+
8
+ describe '#create_reward' do
9
+ it 'creates and stores a reward' do
10
+ reward = engine.create_reward(label: 'bonus', amount: 0.7, delay: 3.0)
11
+ expect(reward).to be_a(Legion::Extensions::TemporalDiscounting::Helpers::Reward)
12
+ expect(engine.rewards).to have_key(reward.id)
13
+ end
14
+
15
+ it 'uses domain rate when no discount_rate given' do
16
+ engine.set_domain_rate(domain: :financial, rate: 0.25)
17
+ reward = engine.create_reward(label: 'fin', amount: 0.5, delay: 2.0, domain: :financial)
18
+ expect(reward.discount_rate).to eq(0.25)
19
+ end
20
+
21
+ it 'uses explicit discount_rate over domain rate' do
22
+ engine.set_domain_rate(domain: :financial, rate: 0.25)
23
+ reward = engine.create_reward(label: 'fin', amount: 0.5, delay: 2.0, domain: :financial, discount_rate: 0.05)
24
+ expect(reward.discount_rate).to eq(0.05)
25
+ end
26
+ end
27
+
28
+ describe '#compare_rewards' do
29
+ it 'returns preferred reward and delta' do
30
+ a = engine.create_reward(label: 'a', amount: 0.9, delay: 1.0, discount_rate: 0.1)
31
+ b = engine.create_reward(label: 'b', amount: 0.3, delay: 1.0, discount_rate: 0.1)
32
+ result = engine.compare_rewards(reward_a_id: a.id, reward_b_id: b.id)
33
+ expect(result[:preferred]).to eq(a.id)
34
+ expect(result[:delta]).to be > 0
35
+ end
36
+
37
+ it 'returns tied when values are equal' do
38
+ a = engine.create_reward(label: 'a', amount: 0.5, delay: 0.0, discount_rate: 0.1)
39
+ b = engine.create_reward(label: 'b', amount: 0.5, delay: 0.0, discount_rate: 0.1)
40
+ result = engine.compare_rewards(reward_a_id: a.id, reward_b_id: b.id)
41
+ expect(result[:preferred]).to eq(:tied)
42
+ end
43
+
44
+ it 'returns error for missing reward' do
45
+ a = engine.create_reward(label: 'a', amount: 0.5, delay: 1.0)
46
+ result = engine.compare_rewards(reward_a_id: a.id, reward_b_id: 'missing')
47
+ expect(result[:error]).to eq(:not_found)
48
+ end
49
+ end
50
+
51
+ describe '#worth_waiting_for?' do
52
+ it 'returns true when subjective_value >= threshold' do
53
+ r = engine.create_reward(label: 'x', amount: 0.9, delay: 0.5, discount_rate: 0.1)
54
+ result = engine.worth_waiting_for?(reward_id: r.id, threshold: 0.3)
55
+ expect(result[:worth_waiting]).to be true
56
+ end
57
+
58
+ it 'returns error for unknown reward' do
59
+ result = engine.worth_waiting_for?(reward_id: 'nope')
60
+ expect(result[:error]).to eq(:not_found)
61
+ end
62
+ end
63
+
64
+ describe '#set_domain_rate / #get_domain_rate' do
65
+ it 'stores and retrieves domain rate' do
66
+ engine.set_domain_rate(domain: :health, rate: 0.3)
67
+ expect(engine.get_domain_rate(:health)).to eq(0.3)
68
+ end
69
+
70
+ it 'returns DEFAULT_DISCOUNT_RATE for unknown domain' do
71
+ expect(engine.get_domain_rate(:unknown)).to eq(Legion::Extensions::TemporalDiscounting::Helpers::DEFAULT_DISCOUNT_RATE)
72
+ end
73
+
74
+ it 'clamps domain rate to MAX' do
75
+ engine.set_domain_rate(domain: :test, rate: 5.0)
76
+ expect(engine.get_domain_rate(:test)).to eq(Legion::Extensions::TemporalDiscounting::Helpers::MAX_DISCOUNT_RATE)
77
+ end
78
+ end
79
+
80
+ describe '#immediate_vs_delayed' do
81
+ it 'prefers immediate when immediate_amount > delayed discounted value' do
82
+ result = engine.immediate_vs_delayed(immediate_amount: 0.8, delayed_amount: 0.9, delay: 100.0)
83
+ expect(result[:preferred]).to eq(:immediate)
84
+ end
85
+
86
+ it 'prefers delayed when delayed discounted value > immediate' do
87
+ result = engine.immediate_vs_delayed(immediate_amount: 0.1, delayed_amount: 0.9, delay: 0.1)
88
+ expect(result[:preferred]).to eq(:delayed)
89
+ end
90
+
91
+ it 'uses domain rate in computation' do
92
+ engine.set_domain_rate(domain: :social, rate: 0.5)
93
+ result = engine.immediate_vs_delayed(immediate_amount: 0.4, delayed_amount: 0.9, delay: 2.0, domain: :social)
94
+ expect(result[:discount_rate]).to eq(0.5)
95
+ end
96
+
97
+ it 'returns delta between values' do
98
+ result = engine.immediate_vs_delayed(immediate_amount: 0.5, delayed_amount: 0.8, delay: 5.0)
99
+ expect(result[:delta]).to be >= 0
100
+ end
101
+ end
102
+
103
+ describe '#optimal_delay' do
104
+ it 'computes maximum delay before value drops below threshold' do
105
+ r = engine.create_reward(label: 'x', amount: 0.8, delay: 1.0, discount_rate: 0.1)
106
+ result = engine.optimal_delay(reward_id: r.id, min_value: 0.4)
107
+ # D = (A/V - 1) / k = (0.8/0.4 - 1) / 0.1 = 1/0.1 = 10
108
+ expect(result[:max_delay]).to be_within(1e-9).of(10.0)
109
+ end
110
+
111
+ it 'returns error when reward not found' do
112
+ result = engine.optimal_delay(reward_id: 'none')
113
+ expect(result[:error]).to eq(:not_found)
114
+ end
115
+
116
+ it 'returns error when min_value > amount' do
117
+ r = engine.create_reward(label: 'x', amount: 0.3, delay: 1.0)
118
+ result = engine.optimal_delay(reward_id: r.id, min_value: 0.5)
119
+ expect(result[:error]).to eq(:threshold_too_high)
120
+ end
121
+ end
122
+
123
+ describe '#patience_report' do
124
+ it 'returns empty report when no rewards' do
125
+ report = engine.patience_report
126
+ expect(report[:total_rewards]).to eq(0)
127
+ expect(report[:avg_discount_rate]).to eq(0.0)
128
+ end
129
+
130
+ it 'computes avg discount rate and subjective value' do
131
+ engine.create_reward(label: 'a', amount: 0.8, delay: 2.0, discount_rate: 0.1)
132
+ engine.create_reward(label: 'b', amount: 0.6, delay: 2.0, discount_rate: 0.3)
133
+ report = engine.patience_report
134
+ expect(report[:total_rewards]).to eq(2)
135
+ expect(report[:avg_discount_rate]).to be_within(1e-9).of(0.2)
136
+ expect(report[:avg_subjective_value]).to be > 0
137
+ end
138
+
139
+ it 'includes impulsivity distribution' do
140
+ engine.create_reward(label: 'a', amount: 0.8, delay: 1.0, discount_rate: 0.02)
141
+ engine.create_reward(label: 'b', amount: 0.8, delay: 1.0, discount_rate: 0.5)
142
+ report = engine.patience_report
143
+ expect(report[:impulsivity_distribution]).to have_key(:patient)
144
+ expect(report[:impulsivity_distribution]).to have_key(:very_impulsive)
145
+ end
146
+ end
147
+
148
+ describe '#rewards_by_domain' do
149
+ it 'filters rewards by domain' do
150
+ engine.create_reward(label: 'fin', amount: 0.5, delay: 1.0, domain: :financial)
151
+ engine.create_reward(label: 'hth', amount: 0.5, delay: 1.0, domain: :health)
152
+ engine.create_reward(label: 'fin2', amount: 0.7, delay: 1.0, domain: :financial)
153
+ result = engine.rewards_by_domain(domain: :financial)
154
+ expect(result.size).to eq(2)
155
+ expect(result.all? { |r| r.domain == :financial }).to be true
156
+ end
157
+ end
158
+
159
+ describe '#most_valuable' do
160
+ it 'returns top rewards by subjective_value' do
161
+ engine.create_reward(label: 'low', amount: 0.1, delay: 100.0, discount_rate: 1.0)
162
+ engine.create_reward(label: 'high', amount: 0.9, delay: 0.1, discount_rate: 0.01)
163
+ result = engine.most_valuable(limit: 1)
164
+ expect(result.first.label).to eq('high')
165
+ end
166
+
167
+ it 'limits the result set' do
168
+ 5.times { |i| engine.create_reward(label: "r#{i}", amount: 0.5, delay: i.to_f + 1.0) }
169
+ expect(engine.most_valuable(limit: 3).size).to eq(3)
170
+ end
171
+ end
172
+
173
+ describe '#prune_expired' do
174
+ it 'removes rewards below min_value threshold' do
175
+ engine.create_reward(label: 'dead', amount: 0.05, delay: 1000.0, discount_rate: 1.0)
176
+ engine.create_reward(label: 'alive', amount: 0.9, delay: 0.1, discount_rate: 0.01)
177
+ result = engine.prune_expired(min_value: 0.05)
178
+ expect(result[:pruned]).to be >= 1
179
+ expect(result[:remaining]).to be >= 1
180
+ end
181
+ end
182
+
183
+ describe '#to_h' do
184
+ it 'returns engine stats hash' do
185
+ engine.create_reward(label: 'x', amount: 0.5, delay: 1.0)
186
+ h = engine.to_h
187
+ expect(h[:total_rewards]).to eq(1)
188
+ expect(h[:domain_rates]).to be_a(Hash)
189
+ expect(h[:patience]).to be_a(Hash)
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/client'
4
+
5
+ RSpec.describe Legion::Extensions::TemporalDiscounting::Helpers::Reward do
6
+ subject(:reward) do
7
+ described_class.new(label: 'test', amount: 0.8, delay: 5.0, domain: :financial, discount_rate: 0.1)
8
+ end
9
+
10
+ describe '#initialize' do
11
+ it 'assigns a UUID id' do
12
+ expect(reward.id).to match(/\A[0-9a-f-]{36}\z/)
13
+ end
14
+
15
+ it 'stores the label' do
16
+ expect(reward.label).to eq('test')
17
+ end
18
+
19
+ it 'clamps amount to [0, 1]' do
20
+ r = described_class.new(label: 'x', amount: 1.5, delay: 1.0)
21
+ expect(r.amount).to eq(1.0)
22
+ end
23
+
24
+ it 'clamps amount below 0 to 0' do
25
+ r = described_class.new(label: 'x', amount: -0.5, delay: 1.0)
26
+ expect(r.amount).to eq(0.0)
27
+ end
28
+
29
+ it 'clamps discount_rate to MIN' do
30
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.0)
31
+ expect(r.discount_rate).to eq(Legion::Extensions::TemporalDiscounting::Helpers::MIN_DISCOUNT_RATE)
32
+ end
33
+
34
+ it 'clamps discount_rate to MAX' do
35
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 2.0)
36
+ expect(r.discount_rate).to eq(Legion::Extensions::TemporalDiscounting::Helpers::MAX_DISCOUNT_RATE)
37
+ end
38
+
39
+ it 'records created_at' do
40
+ expect(reward.created_at).to be_a(Time)
41
+ end
42
+ end
43
+
44
+ describe '#subjective_value' do
45
+ it 'computes hyperbolic discount formula V = A / (1 + k*D)' do
46
+ # A=0.8, k=0.1, D=5.0 => 0.8 / (1 + 0.5) = 0.8 / 1.5
47
+ expected = 0.8 / (1.0 + (0.1 * 5.0))
48
+ expect(reward.subjective_value).to be_within(1e-9).of(expected)
49
+ end
50
+
51
+ it 'equals amount when delay is 0' do
52
+ r = described_class.new(label: 'x', amount: 0.6, delay: 0.0)
53
+ expect(r.subjective_value).to be_within(1e-9).of(0.6)
54
+ end
55
+
56
+ it 'approaches zero as delay grows large' do
57
+ r = described_class.new(label: 'x', amount: 1.0, delay: 10_000.0, discount_rate: 1.0)
58
+ expect(r.subjective_value).to be < 0.001
59
+ end
60
+
61
+ it 'is lower with higher discount rate' do
62
+ r_low = described_class.new(label: 'x', amount: 0.9, delay: 10.0, discount_rate: 0.05)
63
+ r_high = described_class.new(label: 'x', amount: 0.9, delay: 10.0, discount_rate: 0.5)
64
+ expect(r_low.subjective_value).to be > r_high.subjective_value
65
+ end
66
+ end
67
+
68
+ describe '#value_ratio' do
69
+ it 'returns subjective_value / amount' do
70
+ expected = reward.subjective_value / reward.amount
71
+ expect(reward.value_ratio).to be_within(1e-9).of(expected)
72
+ end
73
+
74
+ it 'returns 0.0 when amount is 0' do
75
+ r = described_class.new(label: 'x', amount: 0.0, delay: 1.0)
76
+ expect(r.value_ratio).to eq(0.0)
77
+ end
78
+
79
+ it 'is 1.0 when delay is 0' do
80
+ r = described_class.new(label: 'x', amount: 0.7, delay: 0.0)
81
+ expect(r.value_ratio).to be_within(1e-9).of(1.0)
82
+ end
83
+ end
84
+
85
+ describe '#value_label' do
86
+ it 'returns :full_value when ratio >= 0.8' do
87
+ r = described_class.new(label: 'x', amount: 0.9, delay: 0.0)
88
+ expect(r.value_label).to eq(:full_value)
89
+ end
90
+
91
+ it 'returns :negligible when ratio < 0.2' do
92
+ r = described_class.new(label: 'x', amount: 0.5, delay: 100.0, discount_rate: 1.0)
93
+ expect(r.value_label).to eq(:negligible)
94
+ end
95
+
96
+ it 'returns :moderate_value for mid-range ratio' do
97
+ # A=0.5, k=0.1, D=10 => sv = 0.5/2 = 0.25, ratio = 0.5 => moderate_value
98
+ r = described_class.new(label: 'x', amount: 0.5, delay: 10.0, discount_rate: 0.1)
99
+ ratio = r.value_ratio
100
+ expect(ratio).to be_between(0.4, 0.6)
101
+ expect(r.value_label).to eq(:moderate_value)
102
+ end
103
+ end
104
+
105
+ describe '#impulsivity_label' do
106
+ it 'returns :patient for k < 0.05' do
107
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.02)
108
+ expect(r.impulsivity_label).to eq(:patient)
109
+ end
110
+
111
+ it 'returns :moderate for k in [0.05, 0.15)' do
112
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.1)
113
+ expect(r.impulsivity_label).to eq(:moderate)
114
+ end
115
+
116
+ it 'returns :impulsive for k in [0.15, 0.3)' do
117
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.2)
118
+ expect(r.impulsivity_label).to eq(:impulsive)
119
+ end
120
+
121
+ it 'returns :very_impulsive for k in [0.3, 0.6)' do
122
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.4)
123
+ expect(r.impulsivity_label).to eq(:very_impulsive)
124
+ end
125
+
126
+ it 'returns :extreme for k in [0.6, 1.0]' do
127
+ r = described_class.new(label: 'x', amount: 0.5, delay: 1.0, discount_rate: 0.8)
128
+ expect(r.impulsivity_label).to eq(:extreme)
129
+ end
130
+ end
131
+
132
+ describe '#worth_waiting?' do
133
+ it 'returns true when subjective_value >= default threshold 0.3' do
134
+ r = described_class.new(label: 'x', amount: 0.9, delay: 0.5, discount_rate: 0.1)
135
+ expect(r.worth_waiting?).to be true
136
+ end
137
+
138
+ it 'returns false when subjective_value < threshold' do
139
+ r = described_class.new(label: 'x', amount: 0.1, delay: 100.0, discount_rate: 1.0)
140
+ expect(r.worth_waiting?).to be false
141
+ end
142
+
143
+ it 'accepts custom threshold' do
144
+ r = described_class.new(label: 'x', amount: 0.8, delay: 2.0, discount_rate: 0.1)
145
+ sv = r.subjective_value
146
+ expect(r.worth_waiting?(threshold: sv - 0.01)).to be true
147
+ expect(r.worth_waiting?(threshold: sv + 0.01)).to be false
148
+ end
149
+ end
150
+
151
+ describe '#adjust_delay!' do
152
+ it 'updates delay and changes subjective_value' do
153
+ original_sv = reward.subjective_value
154
+ reward.adjust_delay!(new_delay: 50.0)
155
+ expect(reward.delay).to eq(50.0)
156
+ expect(reward.subjective_value).to be < original_sv
157
+ end
158
+ end
159
+
160
+ describe '#adjust_discount_rate!' do
161
+ it 'updates discount_rate clamped to [MIN, MAX]' do
162
+ reward.adjust_discount_rate!(new_rate: 0.5)
163
+ expect(reward.discount_rate).to eq(0.5)
164
+ end
165
+
166
+ it 'clamps below MIN_DISCOUNT_RATE' do
167
+ reward.adjust_discount_rate!(new_rate: 0.0)
168
+ expect(reward.discount_rate).to eq(Legion::Extensions::TemporalDiscounting::Helpers::MIN_DISCOUNT_RATE)
169
+ end
170
+ end
171
+
172
+ describe '#to_h' do
173
+ it 'returns a complete hash' do
174
+ h = reward.to_h
175
+ expect(h[:id]).to eq(reward.id)
176
+ expect(h[:label]).to eq('test')
177
+ expect(h[:amount]).to eq(0.8)
178
+ expect(h[:delay]).to eq(5.0)
179
+ expect(h[:domain]).to eq(:financial)
180
+ expect(h[:discount_rate]).to eq(0.1)
181
+ expect(h[:subjective_value]).to be_a(Float)
182
+ expect(h[:value_ratio]).to be_a(Float)
183
+ expect(h[:value_label]).to be_a(Symbol)
184
+ expect(h[:impulsivity_label]).to be_a(Symbol)
185
+ expect(h[:worth_waiting]).to be(true).or be(false)
186
+ expect(h[:created_at]).to be_a(Time)
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/temporal_discounting/client'
4
+
5
+ RSpec.describe Legion::Extensions::TemporalDiscounting::Runners::TemporalDiscounting do
6
+ let(:client) { Legion::Extensions::TemporalDiscounting::Client.new }
7
+
8
+ describe '#create_temporal_reward' do
9
+ it 'creates a reward and returns success' do
10
+ result = client.create_temporal_reward(label: 'test', amount: 0.7, delay: 5.0)
11
+ expect(result[:success]).to be true
12
+ expect(result[:reward][:id]).to match(/\A[0-9a-f-]{36}\z/)
13
+ expect(result[:reward][:label]).to eq('test')
14
+ end
15
+
16
+ it 'stores the reward in the engine' do
17
+ result = client.create_temporal_reward(label: 'x', amount: 0.5, delay: 2.0)
18
+ id = result[:reward][:id]
19
+ expect(client.engine.rewards).to have_key(id)
20
+ end
21
+
22
+ it 'passes domain and discount_rate through' do
23
+ result = client.create_temporal_reward(
24
+ label: 'fin', amount: 0.8, delay: 10.0, domain: :financial, discount_rate: 0.25
25
+ )
26
+ expect(result[:reward][:domain]).to eq(:financial)
27
+ expect(result[:reward][:discount_rate]).to eq(0.25)
28
+ end
29
+ end
30
+
31
+ describe '#compare_temporal_rewards' do
32
+ it 'compares two rewards' do
33
+ a = client.create_temporal_reward(label: 'a', amount: 0.9, delay: 1.0, discount_rate: 0.1)
34
+ b = client.create_temporal_reward(label: 'b', amount: 0.2, delay: 1.0, discount_rate: 0.1)
35
+ result = client.compare_temporal_rewards(reward_a_id: a[:reward][:id], reward_b_id: b[:reward][:id])
36
+ expect(result[:success]).to be true
37
+ expect(result[:preferred]).to eq(a[:reward][:id])
38
+ end
39
+
40
+ it 'returns not_found error for missing reward' do
41
+ r = client.create_temporal_reward(label: 'x', amount: 0.5, delay: 1.0)
42
+ result = client.compare_temporal_rewards(reward_a_id: r[:reward][:id], reward_b_id: 'missing')
43
+ expect(result[:error]).to eq(:not_found)
44
+ end
45
+ end
46
+
47
+ describe '#check_worth_waiting' do
48
+ it 'returns worth_waiting true for high-value reward' do
49
+ r = client.create_temporal_reward(label: 'x', amount: 0.9, delay: 0.5, discount_rate: 0.1)
50
+ result = client.check_worth_waiting(reward_id: r[:reward][:id])
51
+ expect(result[:success]).to be true
52
+ expect(result[:worth_waiting]).to be true
53
+ end
54
+
55
+ it 'returns not_found error for missing id' do
56
+ result = client.check_worth_waiting(reward_id: 'missing')
57
+ expect(result[:success]).to be false
58
+ expect(result[:error]).to eq(:not_found)
59
+ end
60
+ end
61
+
62
+ describe '#immediate_vs_delayed_comparison' do
63
+ it 'returns preferred option' do
64
+ result = client.immediate_vs_delayed_comparison(
65
+ immediate_amount: 0.8, delayed_amount: 0.9, delay: 100.0
66
+ )
67
+ expect(result[:success]).to be true
68
+ expect(result[:preferred]).to be_a(Symbol)
69
+ end
70
+
71
+ it 'includes discount_rate in result' do
72
+ result = client.immediate_vs_delayed_comparison(
73
+ immediate_amount: 0.3, delayed_amount: 0.9, delay: 5.0
74
+ )
75
+ expect(result[:discount_rate]).to be_a(Float)
76
+ end
77
+ end
78
+
79
+ describe '#compute_optimal_delay' do
80
+ it 'returns max_delay for a given min_value' do
81
+ r = client.create_temporal_reward(label: 'x', amount: 0.8, delay: 1.0, discount_rate: 0.1)
82
+ result = client.compute_optimal_delay(reward_id: r[:reward][:id], min_value: 0.4)
83
+ expect(result[:success]).to be true
84
+ expect(result[:max_delay]).to be_within(1e-9).of(10.0)
85
+ end
86
+
87
+ it 'returns not_found for missing reward' do
88
+ result = client.compute_optimal_delay(reward_id: 'none')
89
+ expect(result[:success]).to be false
90
+ expect(result[:error]).to eq(:not_found)
91
+ end
92
+ end
93
+
94
+ describe '#temporal_patience_report' do
95
+ it 'returns report with success' do
96
+ client.create_temporal_reward(label: 'a', amount: 0.8, delay: 2.0, discount_rate: 0.1)
97
+ result = client.temporal_patience_report
98
+ expect(result[:success]).to be true
99
+ expect(result[:total_rewards]).to eq(1)
100
+ expect(result[:avg_discount_rate]).to be_a(Float)
101
+ end
102
+ end
103
+
104
+ describe '#set_domain_discount_rate' do
105
+ it 'sets and confirms domain rate' do
106
+ result = client.set_domain_discount_rate(domain: :health, rate: 0.2)
107
+ expect(result[:success]).to be true
108
+ expect(result[:domain]).to eq(:health)
109
+ expect(result[:rate]).to eq(0.2)
110
+ end
111
+ end
112
+
113
+ describe '#most_valuable_rewards' do
114
+ it 'returns top rewards by subjective_value' do
115
+ client.create_temporal_reward(label: 'low', amount: 0.1, delay: 100.0, discount_rate: 1.0)
116
+ client.create_temporal_reward(label: 'high', amount: 0.9, delay: 0.0, discount_rate: 0.1)
117
+ result = client.most_valuable_rewards(limit: 1)
118
+ expect(result[:success]).to be true
119
+ expect(result[:rewards].first[:label]).to eq('high')
120
+ expect(result[:count]).to eq(1)
121
+ end
122
+ end
123
+
124
+ describe '#update_temporal_discounting' do
125
+ it 'prunes low-value rewards and returns stats' do
126
+ client.create_temporal_reward(label: 'dead', amount: 0.02, delay: 1000.0, discount_rate: 1.0)
127
+ client.create_temporal_reward(label: 'alive', amount: 0.9, delay: 0.1, discount_rate: 0.01)
128
+ result = client.update_temporal_discounting(min_value: 0.05)
129
+ expect(result[:success]).to be true
130
+ expect(result[:pruned]).to be >= 1
131
+ expect(result[:stats]).to be_a(Hash)
132
+ end
133
+ end
134
+
135
+ describe '#temporal_discounting_stats' do
136
+ it 'returns engine stats' do
137
+ client.create_temporal_reward(label: 'x', amount: 0.5, delay: 1.0)
138
+ result = client.temporal_discounting_stats
139
+ expect(result[:success]).to be true
140
+ expect(result[:total_rewards]).to eq(1)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+
9
+ def self.info(_msg); end
10
+
11
+ def self.warn(_msg); end
12
+
13
+ def self.error(_msg); end
14
+ end
15
+ end
16
+
17
+ require 'legion/extensions/temporal_discounting'
18
+
19
+ RSpec.configure do |config|
20
+ config.example_status_persistence_file_path = '.rspec_status'
21
+ config.disable_monkey_patching!
22
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
23
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-temporal-discounting
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: Hyperbolic temporal discounting model for brain-modeled agentic AI planning
27
+ and impulse control
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-temporal-discounting.gemspec
36
+ - lib/legion/extensions/temporal_discounting.rb
37
+ - lib/legion/extensions/temporal_discounting/client.rb
38
+ - lib/legion/extensions/temporal_discounting/helpers/constants.rb
39
+ - lib/legion/extensions/temporal_discounting/helpers/discounting_engine.rb
40
+ - lib/legion/extensions/temporal_discounting/helpers/reward.rb
41
+ - lib/legion/extensions/temporal_discounting/runners/temporal_discounting.rb
42
+ - lib/legion/extensions/temporal_discounting/version.rb
43
+ - spec/legion/extensions/temporal_discounting/client_spec.rb
44
+ - spec/legion/extensions/temporal_discounting/helpers/discounting_engine_spec.rb
45
+ - spec/legion/extensions/temporal_discounting/helpers/reward_spec.rb
46
+ - spec/legion/extensions/temporal_discounting/runners/temporal_discounting_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-temporal-discounting
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-temporal-discounting
53
+ source_code_uri: https://github.com/LegionIO/lex-temporal-discounting
54
+ documentation_uri: https://github.com/LegionIO/lex-temporal-discounting
55
+ changelog_uri: https://github.com/LegionIO/lex-temporal-discounting
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-temporal-discounting/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 Temporal Discounting
75
+ test_files: []