lex-uncertainty-tolerance 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: 0bc9fe92cf711e73709939e618754f17efa80d32a5fd4945a0b024f0ba259496
4
+ data.tar.gz: 36f215e7ed043e6f364390ecf291ddaed0801d5af06caf497b3a809d07645617
5
+ SHA512:
6
+ metadata.gz: c562e3a07d513017f0c14cb8886cce7803c63d4350919bdedc5a009a966a320daa9dabba091051deae5772bd66361ab79d61abb05f569198351a9ad9e2a6511f
7
+ data.tar.gz: b53e6eb9b8369d65b38927736f1a59ef26cf77f78274494be4243e857ea4e6761debf6e6fe961421d89222d7960618f8755a48ae591a40277bb4a7c77be6a11c
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+ end
11
+
12
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/uncertainty_tolerance/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-uncertainty-tolerance'
7
+ spec.version = Legion::Extensions::UncertaintyTolerance::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Uncertainty Tolerance'
12
+ spec.description = 'Models individual differences in tolerance for ambiguity and uncertainty for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-uncertainty-tolerance'
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-uncertainty-tolerance'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-uncertainty-tolerance'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-uncertainty-tolerance'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-uncertainty-tolerance/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-uncertainty-tolerance.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/uncertainty_tolerance/helpers/constants'
4
+ require 'legion/extensions/uncertainty_tolerance/helpers/decision'
5
+ require 'legion/extensions/uncertainty_tolerance/helpers/tolerance_engine'
6
+ require 'legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module UncertaintyTolerance
11
+ class Client
12
+ include Runners::UncertaintyTolerance
13
+
14
+ def initialize(**)
15
+ @engine = Helpers::ToleranceEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module UncertaintyTolerance
6
+ module Helpers
7
+ module Constants
8
+ TOLERANCE_LABELS = {
9
+ (0.8..) => :highly_tolerant,
10
+ (0.6...0.8) => :tolerant,
11
+ (0.4...0.6) => :moderate,
12
+ (0.2...0.4) => :intolerant,
13
+ (..0.2) => :highly_intolerant
14
+ }.freeze
15
+
16
+ DECISION_TYPES = %i[certain probable uncertain ambiguous unknown].freeze
17
+
18
+ CERTAINTY_THRESHOLDS = {
19
+ certain: 0.9,
20
+ probable: 0.7,
21
+ uncertain: 0.5,
22
+ ambiguous: 0.3,
23
+ unknown: 0.0
24
+ }.freeze
25
+
26
+ MAX_DECISIONS = 300
27
+ MAX_HISTORY = 500
28
+ DEFAULT_TOLERANCE = 0.5
29
+ TOLERANCE_FLOOR = 0.0
30
+ TOLERANCE_CEILING = 1.0
31
+ ADAPTATION_RATE = 0.05
32
+ POSITIVE_OUTCOME_BOOST = 0.03
33
+ NEGATIVE_OUTCOME_PENALTY = 0.05
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module UncertaintyTolerance
8
+ module Helpers
9
+ class Decision
10
+ attr_reader :id, :description, :domain, :certainty_level,
11
+ :tolerance_at_time, :acted_despite_uncertainty, :created_at
12
+ attr_accessor :actual_outcome
13
+
14
+ def initialize(description:, domain:, certainty_level:, tolerance_at_time:)
15
+ @id = SecureRandom.uuid
16
+ @description = description
17
+ @domain = domain
18
+ @certainty_level = certainty_level.clamp(0.0, 1.0)
19
+ @tolerance_at_time = tolerance_at_time
20
+ @actual_outcome = nil
21
+ @acted_despite_uncertainty = certainty_level < tolerance_at_time
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def resolve!(outcome:)
26
+ @actual_outcome = outcome
27
+ self
28
+ end
29
+
30
+ def successful?
31
+ @actual_outcome == :success
32
+ end
33
+
34
+ def risky?
35
+ @certainty_level < 0.4
36
+ end
37
+
38
+ def decision_type
39
+ Constants::CERTAINTY_THRESHOLDS.each do |type, threshold|
40
+ return type if @certainty_level >= threshold
41
+ end
42
+ :unknown
43
+ end
44
+
45
+ def to_h
46
+ {
47
+ id: @id,
48
+ description: @description,
49
+ domain: @domain,
50
+ certainty_level: @certainty_level,
51
+ actual_outcome: @actual_outcome,
52
+ tolerance_at_time: @tolerance_at_time,
53
+ decision_type: decision_type,
54
+ acted_despite_uncertainty: @acted_despite_uncertainty,
55
+ created_at: @created_at
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module UncertaintyTolerance
6
+ module Helpers
7
+ class ToleranceEngine
8
+ attr_reader :current_tolerance, :decisions, :history
9
+
10
+ def initialize(initial_tolerance: Constants::DEFAULT_TOLERANCE)
11
+ @current_tolerance = initial_tolerance.clamp(
12
+ Constants::TOLERANCE_FLOOR,
13
+ Constants::TOLERANCE_CEILING
14
+ )
15
+ @decisions = {}
16
+ @history = []
17
+ end
18
+
19
+ def tolerance_label
20
+ Constants::TOLERANCE_LABELS.each do |range, label|
21
+ return label if range.cover?(@current_tolerance)
22
+ end
23
+ :unknown
24
+ end
25
+
26
+ def record_decision(description:, certainty_level:, domain: :general)
27
+ decision = Decision.new(
28
+ description: description,
29
+ domain: domain,
30
+ certainty_level: certainty_level,
31
+ tolerance_at_time: @current_tolerance
32
+ )
33
+ @decisions[decision.id] = decision
34
+ prune_decisions
35
+ decision
36
+ end
37
+
38
+ def resolve_decision(decision_id:, outcome:)
39
+ decision = @decisions[decision_id]
40
+ return nil unless decision
41
+
42
+ decision.resolve!(outcome: outcome)
43
+ record_history(decision)
44
+ adapt_tolerance(decision)
45
+ decision
46
+ end
47
+
48
+ def decisions_under_uncertainty(threshold: nil)
49
+ cutoff = threshold || @current_tolerance
50
+ @decisions.values.select { |d| d.certainty_level < cutoff }
51
+ end
52
+
53
+ def successful_uncertain_decisions
54
+ @decisions.values.select do |d|
55
+ d.acted_despite_uncertainty && d.successful?
56
+ end
57
+ end
58
+
59
+ def risk_profile
60
+ breakdown = Constants::DECISION_TYPES.to_h { |t| [t, 0] }
61
+ @decisions.each_value { |d| breakdown[d.decision_type] += 1 }
62
+ breakdown
63
+ end
64
+
65
+ def domain_tolerance(domain:)
66
+ resolved = @decisions.values.select do |d|
67
+ d.domain == domain && d.successful? && !d.actual_outcome.nil?
68
+ end
69
+ return nil if resolved.empty?
70
+
71
+ resolved.sum(&:certainty_level) / resolved.size
72
+ end
73
+
74
+ def should_act?(certainty:)
75
+ certainty >= @current_tolerance
76
+ end
77
+
78
+ def comfort_zone_expansion_rate
79
+ return 0.0 if @history.size < 2
80
+
81
+ tolerances = @history.last(10).map { |h| h[:tolerance_snapshot] }
82
+ return 0.0 if tolerances.size < 2
83
+
84
+ (tolerances.last - tolerances.first) / (tolerances.size - 1).to_f
85
+ end
86
+
87
+ def to_h
88
+ {
89
+ current_tolerance: @current_tolerance,
90
+ tolerance_label: tolerance_label,
91
+ total_decisions: @decisions.size,
92
+ risk_profile: risk_profile,
93
+ history_count: @history.size
94
+ }
95
+ end
96
+
97
+ private
98
+
99
+ def adapt_tolerance(decision)
100
+ return unless decision.acted_despite_uncertainty
101
+
102
+ delta = if decision.successful?
103
+ Constants::POSITIVE_OUTCOME_BOOST
104
+ else
105
+ -Constants::NEGATIVE_OUTCOME_PENALTY
106
+ end
107
+
108
+ @current_tolerance = (@current_tolerance + delta).clamp(
109
+ Constants::TOLERANCE_FLOOR,
110
+ Constants::TOLERANCE_CEILING
111
+ )
112
+ end
113
+
114
+ def record_history(decision)
115
+ @history << {
116
+ decision_id: decision.id,
117
+ outcome: decision.actual_outcome,
118
+ certainty_level: decision.certainty_level,
119
+ tolerance_snapshot: @current_tolerance,
120
+ recorded_at: Time.now.utc
121
+ }
122
+ @history.shift while @history.size > Constants::MAX_HISTORY
123
+ end
124
+
125
+ def prune_decisions
126
+ return unless @decisions.size > Constants::MAX_DECISIONS
127
+
128
+ oldest_keys = @decisions.keys.first(@decisions.size - Constants::MAX_DECISIONS)
129
+ oldest_keys.each { |k| @decisions.delete(k) }
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module UncertaintyTolerance
6
+ module Runners
7
+ module UncertaintyTolerance
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def record_uncertain_decision(description:, certainty_level:, domain: :general, **)
12
+ decision = engine.record_decision(
13
+ description: description,
14
+ domain: domain,
15
+ certainty_level: certainty_level
16
+ )
17
+ Legion::Logging.debug "[uncertainty_tolerance] recorded decision: id=#{decision.id[0..7]} " \
18
+ "domain=#{domain} certainty=#{certainty_level.round(2)} " \
19
+ "type=#{decision.decision_type}"
20
+ {
21
+ decision_id: decision.id,
22
+ domain: domain,
23
+ certainty_level: certainty_level,
24
+ decision_type: decision.decision_type,
25
+ acted_despite_uncertainty: decision.acted_despite_uncertainty,
26
+ current_tolerance: engine.current_tolerance
27
+ }
28
+ end
29
+
30
+ def resolve_uncertain_decision(decision_id:, outcome:, **)
31
+ decision = engine.resolve_decision(decision_id: decision_id, outcome: outcome)
32
+ unless decision
33
+ Legion::Logging.debug "[uncertainty_tolerance] resolve failed: #{decision_id[0..7]} not found"
34
+ return { resolved: false, reason: :not_found }
35
+ end
36
+
37
+ Legion::Logging.info "[uncertainty_tolerance] resolved: id=#{decision_id[0..7]} " \
38
+ "outcome=#{outcome} tolerance=#{engine.current_tolerance.round(3)}"
39
+ {
40
+ resolved: true,
41
+ decision_id: decision_id,
42
+ outcome: outcome,
43
+ current_tolerance: engine.current_tolerance,
44
+ tolerance_label: engine.tolerance_label
45
+ }
46
+ end
47
+
48
+ def should_act_assessment(certainty:, **)
49
+ act = engine.should_act?(certainty: certainty)
50
+ gap = (certainty - engine.current_tolerance).round(3)
51
+ Legion::Logging.debug "[uncertainty_tolerance] should_act? certainty=#{certainty.round(2)} " \
52
+ "tolerance=#{engine.current_tolerance.round(2)} act=#{act}"
53
+ {
54
+ should_act: act,
55
+ certainty: certainty,
56
+ current_tolerance: engine.current_tolerance,
57
+ tolerance_label: engine.tolerance_label,
58
+ gap: gap
59
+ }
60
+ end
61
+
62
+ def uncertainty_profile(**)
63
+ profile = engine.to_h
64
+ Legion::Logging.debug "[uncertainty_tolerance] profile: tolerance=#{profile[:current_tolerance].round(3)} " \
65
+ "label=#{profile[:tolerance_label]} decisions=#{profile[:total_decisions]}"
66
+ profile
67
+ end
68
+
69
+ def decisions_under_uncertainty_report(threshold: nil, **)
70
+ decisions = engine.decisions_under_uncertainty(threshold: threshold)
71
+ Legion::Logging.debug "[uncertainty_tolerance] under_uncertainty: count=#{decisions.size}"
72
+ {
73
+ decisions: decisions.map(&:to_h),
74
+ count: decisions.size,
75
+ threshold: threshold || engine.current_tolerance
76
+ }
77
+ end
78
+
79
+ def domain_tolerance_report(domain:, **)
80
+ avg = engine.domain_tolerance(domain: domain)
81
+ Legion::Logging.debug "[uncertainty_tolerance] domain_tolerance: domain=#{domain} avg=#{avg&.round(3)}"
82
+ {
83
+ domain: domain,
84
+ average_certainty: avg,
85
+ found: !avg.nil?
86
+ }
87
+ end
88
+
89
+ def update_uncertainty_tolerance(tolerance:, **)
90
+ clamped = tolerance.clamp(
91
+ Helpers::Constants::TOLERANCE_FLOOR,
92
+ Helpers::Constants::TOLERANCE_CEILING
93
+ )
94
+ engine.instance_variable_set(:@current_tolerance, clamped)
95
+ Legion::Logging.info "[uncertainty_tolerance] tolerance updated: #{clamped.round(3)} " \
96
+ "label=#{engine.tolerance_label}"
97
+ {
98
+ updated: true,
99
+ current_tolerance: clamped,
100
+ tolerance_label: engine.tolerance_label
101
+ }
102
+ end
103
+
104
+ def uncertainty_tolerance_stats(**)
105
+ {
106
+ current_tolerance: engine.current_tolerance,
107
+ tolerance_label: engine.tolerance_label,
108
+ total_decisions: engine.decisions.size,
109
+ successful_uncertain_count: engine.successful_uncertain_decisions.size,
110
+ risk_profile: engine.risk_profile,
111
+ comfort_zone_expansion_rate: engine.comfort_zone_expansion_rate,
112
+ history_count: engine.history.size
113
+ }
114
+ end
115
+
116
+ private
117
+
118
+ def engine
119
+ @engine ||= Helpers::ToleranceEngine.new
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module UncertaintyTolerance
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/uncertainty_tolerance/version'
4
+ require 'legion/extensions/uncertainty_tolerance/helpers/constants'
5
+ require 'legion/extensions/uncertainty_tolerance/helpers/decision'
6
+ require 'legion/extensions/uncertainty_tolerance/helpers/tolerance_engine'
7
+ require 'legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module UncertaintyTolerance
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/uncertainty_tolerance/client'
4
+
5
+ RSpec.describe Legion::Extensions::UncertaintyTolerance::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:record_uncertain_decision)
10
+ expect(client).to respond_to(:resolve_uncertain_decision)
11
+ expect(client).to respond_to(:should_act_assessment)
12
+ expect(client).to respond_to(:uncertainty_profile)
13
+ expect(client).to respond_to(:decisions_under_uncertainty_report)
14
+ expect(client).to respond_to(:domain_tolerance_report)
15
+ expect(client).to respond_to(:update_uncertainty_tolerance)
16
+ expect(client).to respond_to(:uncertainty_tolerance_stats)
17
+ end
18
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::UncertaintyTolerance::Helpers::Constants do
4
+ describe 'TOLERANCE_LABELS' do
5
+ subject(:labels) { described_class::TOLERANCE_LABELS }
6
+
7
+ it 'maps 0.9 to :highly_tolerant' do
8
+ match = labels.find { |range, _| range.cover?(0.9) }
9
+ expect(match.last).to eq(:highly_tolerant)
10
+ end
11
+
12
+ it 'maps 0.7 to :tolerant' do
13
+ match = labels.find { |range, _| range.cover?(0.7) }
14
+ expect(match.last).to eq(:tolerant)
15
+ end
16
+
17
+ it 'maps 0.5 to :moderate' do
18
+ match = labels.find { |range, _| range.cover?(0.5) }
19
+ expect(match.last).to eq(:moderate)
20
+ end
21
+
22
+ it 'maps 0.3 to :intolerant' do
23
+ match = labels.find { |range, _| range.cover?(0.3) }
24
+ expect(match.last).to eq(:intolerant)
25
+ end
26
+
27
+ it 'maps 0.1 to :highly_intolerant' do
28
+ match = labels.find { |range, _| range.cover?(0.1) }
29
+ expect(match.last).to eq(:highly_intolerant)
30
+ end
31
+ end
32
+
33
+ describe 'DECISION_TYPES' do
34
+ it 'contains exactly 5 types' do
35
+ expect(described_class::DECISION_TYPES.size).to eq(5)
36
+ end
37
+
38
+ it 'includes :unknown' do
39
+ expect(described_class::DECISION_TYPES).to include(:unknown)
40
+ end
41
+ end
42
+
43
+ describe 'CERTAINTY_THRESHOLDS' do
44
+ it 'has :certain at 0.9' do
45
+ expect(described_class::CERTAINTY_THRESHOLDS[:certain]).to eq(0.9)
46
+ end
47
+
48
+ it 'has :unknown at 0.0' do
49
+ expect(described_class::CERTAINTY_THRESHOLDS[:unknown]).to eq(0.0)
50
+ end
51
+ end
52
+
53
+ describe 'numeric constants' do
54
+ it 'DEFAULT_TOLERANCE is 0.5' do
55
+ expect(described_class::DEFAULT_TOLERANCE).to eq(0.5)
56
+ end
57
+
58
+ it 'POSITIVE_OUTCOME_BOOST is less than NEGATIVE_OUTCOME_PENALTY' do
59
+ expect(described_class::POSITIVE_OUTCOME_BOOST).to be < described_class::NEGATIVE_OUTCOME_PENALTY
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::UncertaintyTolerance::Helpers::Decision do
4
+ let(:decision) do
5
+ described_class.new(
6
+ description: 'deploy to production',
7
+ domain: :ops,
8
+ certainty_level: 0.6,
9
+ tolerance_at_time: 0.5
10
+ )
11
+ end
12
+
13
+ describe '#initialize' do
14
+ it 'assigns a uuid id' do
15
+ expect(decision.id).to match(/\A[0-9a-f-]{36}\z/)
16
+ end
17
+
18
+ it 'clamps certainty_level to [0, 1]' do
19
+ d = described_class.new(
20
+ description: 'test', domain: :test, certainty_level: 1.5, tolerance_at_time: 0.5
21
+ )
22
+ expect(d.certainty_level).to eq(1.0)
23
+ end
24
+
25
+ it 'sets acted_despite_uncertainty true when certainty < tolerance' do
26
+ d = described_class.new(
27
+ description: 'risky', domain: :test, certainty_level: 0.3, tolerance_at_time: 0.5
28
+ )
29
+ expect(d.acted_despite_uncertainty).to be true
30
+ end
31
+
32
+ it 'sets acted_despite_uncertainty false when certainty >= tolerance' do
33
+ expect(decision.acted_despite_uncertainty).to be false
34
+ end
35
+
36
+ it 'starts with nil actual_outcome' do
37
+ expect(decision.actual_outcome).to be_nil
38
+ end
39
+ end
40
+
41
+ describe '#resolve!' do
42
+ it 'sets actual_outcome and returns self' do
43
+ result = decision.resolve!(outcome: :success)
44
+ expect(decision.actual_outcome).to eq(:success)
45
+ expect(result).to be(decision)
46
+ end
47
+ end
48
+
49
+ describe '#successful?' do
50
+ it 'returns true after :success outcome' do
51
+ decision.resolve!(outcome: :success)
52
+ expect(decision.successful?).to be true
53
+ end
54
+
55
+ it 'returns false after :failure outcome' do
56
+ decision.resolve!(outcome: :failure)
57
+ expect(decision.successful?).to be false
58
+ end
59
+
60
+ it 'returns false when unresolved' do
61
+ expect(decision.successful?).to be false
62
+ end
63
+ end
64
+
65
+ describe '#risky?' do
66
+ it 'returns true when certainty < 0.4' do
67
+ d = described_class.new(
68
+ description: 'risky', domain: :test, certainty_level: 0.3, tolerance_at_time: 0.5
69
+ )
70
+ expect(d.risky?).to be true
71
+ end
72
+
73
+ it 'returns false when certainty >= 0.4' do
74
+ expect(decision.risky?).to be false
75
+ end
76
+ end
77
+
78
+ describe '#decision_type' do
79
+ it 'returns :certain for certainty 0.95' do
80
+ d = described_class.new(
81
+ description: 'test', domain: :test, certainty_level: 0.95, tolerance_at_time: 0.5
82
+ )
83
+ expect(d.decision_type).to eq(:certain)
84
+ end
85
+
86
+ it 'returns :probable for certainty 0.75' do
87
+ d = described_class.new(
88
+ description: 'test', domain: :test, certainty_level: 0.75, tolerance_at_time: 0.5
89
+ )
90
+ expect(d.decision_type).to eq(:probable)
91
+ end
92
+
93
+ it 'returns :uncertain for certainty 0.55' do
94
+ d = described_class.new(
95
+ description: 'test', domain: :test, certainty_level: 0.55, tolerance_at_time: 0.5
96
+ )
97
+ expect(d.decision_type).to eq(:uncertain)
98
+ end
99
+
100
+ it 'returns :ambiguous for certainty 0.35' do
101
+ d = described_class.new(
102
+ description: 'test', domain: :test, certainty_level: 0.35, tolerance_at_time: 0.5
103
+ )
104
+ expect(d.decision_type).to eq(:ambiguous)
105
+ end
106
+
107
+ it 'returns :unknown for certainty 0.0' do
108
+ d = described_class.new(
109
+ description: 'test', domain: :test, certainty_level: 0.0, tolerance_at_time: 0.5
110
+ )
111
+ expect(d.decision_type).to eq(:unknown)
112
+ end
113
+ end
114
+
115
+ describe '#to_h' do
116
+ it 'includes all expected keys' do
117
+ h = decision.to_h
118
+ expect(h.keys).to include(
119
+ :id, :description, :domain, :certainty_level,
120
+ :actual_outcome, :tolerance_at_time, :decision_type,
121
+ :acted_despite_uncertainty, :created_at
122
+ )
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::UncertaintyTolerance::Helpers::ToleranceEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts at DEFAULT_TOLERANCE' do
8
+ expect(engine.current_tolerance).to eq(
9
+ Legion::Extensions::UncertaintyTolerance::Helpers::Constants::DEFAULT_TOLERANCE
10
+ )
11
+ end
12
+
13
+ it 'accepts a custom initial_tolerance' do
14
+ e = described_class.new(initial_tolerance: 0.8)
15
+ expect(e.current_tolerance).to eq(0.8)
16
+ end
17
+
18
+ it 'clamps initial_tolerance to [0, 1]' do
19
+ e = described_class.new(initial_tolerance: 1.5)
20
+ expect(e.current_tolerance).to eq(1.0)
21
+ end
22
+ end
23
+
24
+ describe '#tolerance_label' do
25
+ it 'returns :moderate at 0.5' do
26
+ expect(engine.tolerance_label).to eq(:moderate)
27
+ end
28
+
29
+ it 'returns :highly_tolerant at 0.9' do
30
+ e = described_class.new(initial_tolerance: 0.9)
31
+ expect(e.tolerance_label).to eq(:highly_tolerant)
32
+ end
33
+ end
34
+
35
+ describe '#record_decision' do
36
+ it 'creates and stores a decision' do
37
+ decision = engine.record_decision(
38
+ description: 'test', domain: :ops, certainty_level: 0.4
39
+ )
40
+ expect(engine.decisions[decision.id]).to be(decision)
41
+ end
42
+
43
+ it 'returns a Decision object' do
44
+ result = engine.record_decision(description: 'test', certainty_level: 0.6)
45
+ expect(result).to be_a(Legion::Extensions::UncertaintyTolerance::Helpers::Decision)
46
+ end
47
+
48
+ it 'defaults domain to :general' do
49
+ decision = engine.record_decision(description: 'test', certainty_level: 0.6)
50
+ expect(decision.domain).to eq(:general)
51
+ end
52
+ end
53
+
54
+ describe '#resolve_decision' do
55
+ let(:decision) { engine.record_decision(description: 'test', certainty_level: 0.3) }
56
+
57
+ it 'returns nil for unknown decision_id' do
58
+ expect(engine.resolve_decision(decision_id: 'nonexistent', outcome: :success)).to be_nil
59
+ end
60
+
61
+ it 'resolves a known decision' do
62
+ result = engine.resolve_decision(decision_id: decision.id, outcome: :success)
63
+ expect(result.actual_outcome).to eq(:success)
64
+ end
65
+
66
+ it 'boosts tolerance on success under uncertainty' do
67
+ engine_low = described_class.new(initial_tolerance: 0.8)
68
+ d = engine_low.record_decision(description: 'uncertain act', certainty_level: 0.3)
69
+ before = engine_low.current_tolerance
70
+ engine_low.resolve_decision(decision_id: d.id, outcome: :success)
71
+ expect(engine_low.current_tolerance).to be > before
72
+ end
73
+
74
+ it 'penalizes tolerance on failure under uncertainty' do
75
+ engine_low = described_class.new(initial_tolerance: 0.8)
76
+ d = engine_low.record_decision(description: 'uncertain act', certainty_level: 0.3)
77
+ before = engine_low.current_tolerance
78
+ engine_low.resolve_decision(decision_id: d.id, outcome: :failure)
79
+ expect(engine_low.current_tolerance).to be < before
80
+ end
81
+
82
+ it 'does not adapt tolerance when certainty >= tolerance (no uncertainty)' do
83
+ d = engine.record_decision(description: 'certain act', certainty_level: 0.9)
84
+ before = engine.current_tolerance
85
+ engine.resolve_decision(decision_id: d.id, outcome: :success)
86
+ expect(engine.current_tolerance).to eq(before)
87
+ end
88
+ end
89
+
90
+ describe '#decisions_under_uncertainty' do
91
+ before do
92
+ engine.record_decision(description: 'low', certainty_level: 0.2)
93
+ engine.record_decision(description: 'high', certainty_level: 0.9)
94
+ end
95
+
96
+ it 'returns decisions below the current tolerance' do
97
+ result = engine.decisions_under_uncertainty
98
+ expect(result.map(&:description)).to include('low')
99
+ expect(result.map(&:description)).not_to include('high')
100
+ end
101
+
102
+ it 'accepts a custom threshold' do
103
+ result = engine.decisions_under_uncertainty(threshold: 0.3)
104
+ expect(result.all? { |d| d.certainty_level < 0.3 }).to be true
105
+ end
106
+ end
107
+
108
+ describe '#successful_uncertain_decisions' do
109
+ it 'returns only resolved successes where agent acted despite uncertainty' do
110
+ e = described_class.new(initial_tolerance: 0.8)
111
+ d = e.record_decision(description: 'risky success', certainty_level: 0.3)
112
+ e.resolve_decision(decision_id: d.id, outcome: :success)
113
+ expect(e.successful_uncertain_decisions.size).to eq(1)
114
+ end
115
+
116
+ it 'excludes failures' do
117
+ e = described_class.new(initial_tolerance: 0.8)
118
+ d = e.record_decision(description: 'risky fail', certainty_level: 0.3)
119
+ e.resolve_decision(decision_id: d.id, outcome: :failure)
120
+ expect(e.successful_uncertain_decisions).to be_empty
121
+ end
122
+ end
123
+
124
+ describe '#risk_profile' do
125
+ it 'returns a hash keyed by decision types' do
126
+ engine.record_decision(description: 'c', certainty_level: 0.95)
127
+ profile = engine.risk_profile
128
+ expect(profile.keys).to match_array(
129
+ Legion::Extensions::UncertaintyTolerance::Helpers::Constants::DECISION_TYPES
130
+ )
131
+ end
132
+
133
+ it 'counts decisions correctly' do
134
+ 2.times { engine.record_decision(description: 'x', certainty_level: 0.95) }
135
+ expect(engine.risk_profile[:certain]).to eq(2)
136
+ end
137
+ end
138
+
139
+ describe '#domain_tolerance' do
140
+ it 'returns nil when no resolved decisions for domain' do
141
+ expect(engine.domain_tolerance(domain: :missing)).to be_nil
142
+ end
143
+
144
+ it 'returns average certainty of successful decisions in domain' do
145
+ d = engine.record_decision(description: 'x', domain: :code, certainty_level: 0.8)
146
+ engine.resolve_decision(decision_id: d.id, outcome: :success)
147
+ expect(engine.domain_tolerance(domain: :code)).to be_within(0.001).of(0.8)
148
+ end
149
+ end
150
+
151
+ describe '#should_act?' do
152
+ it 'returns true when certainty >= current_tolerance' do
153
+ expect(engine.should_act?(certainty: 0.5)).to be true
154
+ end
155
+
156
+ it 'returns false when certainty < current_tolerance' do
157
+ expect(engine.should_act?(certainty: 0.3)).to be false
158
+ end
159
+ end
160
+
161
+ describe '#comfort_zone_expansion_rate' do
162
+ it 'returns 0.0 with fewer than 2 history entries' do
163
+ expect(engine.comfort_zone_expansion_rate).to eq(0.0)
164
+ end
165
+
166
+ it 'returns positive rate after multiple successful uncertain decisions' do
167
+ e = described_class.new(initial_tolerance: 0.6)
168
+ 5.times do
169
+ d = e.record_decision(description: 'test', certainty_level: 0.2)
170
+ e.resolve_decision(decision_id: d.id, outcome: :success)
171
+ end
172
+ expect(e.comfort_zone_expansion_rate).to be >= 0.0
173
+ end
174
+ end
175
+
176
+ describe '#to_h' do
177
+ it 'includes required keys' do
178
+ h = engine.to_h
179
+ expect(h.keys).to include(
180
+ :current_tolerance, :tolerance_label, :total_decisions, :risk_profile, :history_count
181
+ )
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/uncertainty_tolerance/client'
4
+
5
+ RSpec.describe Legion::Extensions::UncertaintyTolerance::Runners::UncertaintyTolerance do
6
+ let(:client) { Legion::Extensions::UncertaintyTolerance::Client.new }
7
+
8
+ describe '#record_uncertain_decision' do
9
+ it 'records a decision and returns structured response' do
10
+ result = client.record_uncertain_decision(
11
+ description: 'deploy service',
12
+ certainty_level: 0.4,
13
+ domain: :ops
14
+ )
15
+ expect(result[:decision_id]).to match(/\A[0-9a-f-]{36}\z/)
16
+ expect(result[:certainty_level]).to eq(0.4)
17
+ expect(result[:domain]).to eq(:ops)
18
+ expect(result[:current_tolerance]).to be_a(Float)
19
+ end
20
+
21
+ it 'computes acted_despite_uncertainty flag' do
22
+ result = client.record_uncertain_decision(
23
+ description: 'risky action',
24
+ certainty_level: 0.2
25
+ )
26
+ expect(result[:acted_despite_uncertainty]).to be true
27
+ end
28
+
29
+ it 'includes decision_type in response' do
30
+ result = client.record_uncertain_decision(
31
+ description: 'probable action',
32
+ certainty_level: 0.75
33
+ )
34
+ expect(result[:decision_type]).to eq(:probable)
35
+ end
36
+ end
37
+
38
+ describe '#resolve_uncertain_decision' do
39
+ let(:decision_id) do
40
+ client.record_uncertain_decision(
41
+ description: 'test', certainty_level: 0.3
42
+ )[:decision_id]
43
+ end
44
+
45
+ it 'resolves a known decision' do
46
+ result = client.resolve_uncertain_decision(decision_id: decision_id, outcome: :success)
47
+ expect(result[:resolved]).to be true
48
+ expect(result[:outcome]).to eq(:success)
49
+ end
50
+
51
+ it 'returns resolved: false for unknown id' do
52
+ result = client.resolve_uncertain_decision(decision_id: 'bad-id', outcome: :success)
53
+ expect(result[:resolved]).to be false
54
+ expect(result[:reason]).to eq(:not_found)
55
+ end
56
+
57
+ it 'includes updated tolerance in response' do
58
+ result = client.resolve_uncertain_decision(decision_id: decision_id, outcome: :success)
59
+ expect(result[:current_tolerance]).to be_a(Float)
60
+ expect(result[:tolerance_label]).to be_a(Symbol)
61
+ end
62
+ end
63
+
64
+ describe '#should_act_assessment' do
65
+ it 'returns should_act: true when certainty meets tolerance' do
66
+ result = client.should_act_assessment(certainty: 0.9)
67
+ expect(result[:should_act]).to be true
68
+ end
69
+
70
+ it 'returns should_act: false when certainty is below tolerance' do
71
+ result = client.should_act_assessment(certainty: 0.1)
72
+ expect(result[:should_act]).to be false
73
+ end
74
+
75
+ it 'includes gap in response' do
76
+ result = client.should_act_assessment(certainty: 0.7)
77
+ expect(result[:gap]).to be_a(Float)
78
+ end
79
+ end
80
+
81
+ describe '#uncertainty_profile' do
82
+ it 'returns profile hash with expected keys' do
83
+ result = client.uncertainty_profile
84
+ expect(result.keys).to include(
85
+ :current_tolerance, :tolerance_label, :total_decisions, :risk_profile
86
+ )
87
+ end
88
+ end
89
+
90
+ describe '#decisions_under_uncertainty_report' do
91
+ before do
92
+ client.record_uncertain_decision(description: 'low certainty', certainty_level: 0.1)
93
+ client.record_uncertain_decision(description: 'high certainty', certainty_level: 0.9)
94
+ end
95
+
96
+ it 'returns decisions below threshold' do
97
+ result = client.decisions_under_uncertainty_report
98
+ expect(result[:count]).to be >= 1
99
+ expect(result[:decisions]).to all(include(:certainty_level))
100
+ end
101
+
102
+ it 'accepts a custom threshold' do
103
+ result = client.decisions_under_uncertainty_report(threshold: 0.2)
104
+ expect(result[:threshold]).to eq(0.2)
105
+ end
106
+ end
107
+
108
+ describe '#domain_tolerance_report' do
109
+ it 'returns found: false when domain has no data' do
110
+ result = client.domain_tolerance_report(domain: :unknown_domain)
111
+ expect(result[:found]).to be false
112
+ end
113
+
114
+ it 'returns average certainty when successful decisions exist' do
115
+ id = client.record_uncertain_decision(
116
+ description: 'code review', domain: :code, certainty_level: 0.85
117
+ )[:decision_id]
118
+ client.resolve_uncertain_decision(decision_id: id, outcome: :success)
119
+ result = client.domain_tolerance_report(domain: :code)
120
+ expect(result[:found]).to be true
121
+ expect(result[:average_certainty]).to be_within(0.01).of(0.85)
122
+ end
123
+ end
124
+
125
+ describe '#update_uncertainty_tolerance' do
126
+ it 'updates tolerance to clamped value' do
127
+ result = client.update_uncertainty_tolerance(tolerance: 0.7)
128
+ expect(result[:updated]).to be true
129
+ expect(result[:current_tolerance]).to eq(0.7)
130
+ end
131
+
132
+ it 'clamps values above 1.0' do
133
+ result = client.update_uncertainty_tolerance(tolerance: 1.5)
134
+ expect(result[:current_tolerance]).to eq(1.0)
135
+ end
136
+
137
+ it 'clamps values below 0.0' do
138
+ result = client.update_uncertainty_tolerance(tolerance: -0.5)
139
+ expect(result[:current_tolerance]).to eq(0.0)
140
+ end
141
+ end
142
+
143
+ describe '#uncertainty_tolerance_stats' do
144
+ it 'returns comprehensive stats' do
145
+ result = client.uncertainty_tolerance_stats
146
+ expect(result.keys).to include(
147
+ :current_tolerance,
148
+ :tolerance_label,
149
+ :total_decisions,
150
+ :successful_uncertain_count,
151
+ :risk_profile,
152
+ :comfort_zone_expansion_rate,
153
+ :history_count
154
+ )
155
+ end
156
+ end
157
+ 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/uncertainty_tolerance'
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-uncertainty-tolerance
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: Models individual differences in tolerance for ambiguity and uncertainty
27
+ for brain-modeled agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-uncertainty-tolerance.gemspec
36
+ - lib/legion/extensions/uncertainty_tolerance.rb
37
+ - lib/legion/extensions/uncertainty_tolerance/client.rb
38
+ - lib/legion/extensions/uncertainty_tolerance/helpers/constants.rb
39
+ - lib/legion/extensions/uncertainty_tolerance/helpers/decision.rb
40
+ - lib/legion/extensions/uncertainty_tolerance/helpers/tolerance_engine.rb
41
+ - lib/legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance.rb
42
+ - lib/legion/extensions/uncertainty_tolerance/version.rb
43
+ - spec/legion/extensions/uncertainty_tolerance/client_spec.rb
44
+ - spec/legion/extensions/uncertainty_tolerance/helpers/constants_spec.rb
45
+ - spec/legion/extensions/uncertainty_tolerance/helpers/decision_spec.rb
46
+ - spec/legion/extensions/uncertainty_tolerance/helpers/tolerance_engine_spec.rb
47
+ - spec/legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-uncertainty-tolerance
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-uncertainty-tolerance
54
+ source_code_uri: https://github.com/LegionIO/lex-uncertainty-tolerance
55
+ documentation_uri: https://github.com/LegionIO/lex-uncertainty-tolerance
56
+ changelog_uri: https://github.com/LegionIO/lex-uncertainty-tolerance
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-uncertainty-tolerance/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 Uncertainty Tolerance
76
+ test_files: []