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 +7 -0
- data/Gemfile +12 -0
- data/lex-uncertainty-tolerance.gemspec +29 -0
- data/lib/legion/extensions/uncertainty_tolerance/client.rb +24 -0
- data/lib/legion/extensions/uncertainty_tolerance/helpers/constants.rb +38 -0
- data/lib/legion/extensions/uncertainty_tolerance/helpers/decision.rb +62 -0
- data/lib/legion/extensions/uncertainty_tolerance/helpers/tolerance_engine.rb +135 -0
- data/lib/legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance.rb +125 -0
- data/lib/legion/extensions/uncertainty_tolerance/version.rb +9 -0
- data/lib/legion/extensions/uncertainty_tolerance.rb +15 -0
- data/spec/legion/extensions/uncertainty_tolerance/client_spec.rb +18 -0
- data/spec/legion/extensions/uncertainty_tolerance/helpers/constants_spec.rb +62 -0
- data/spec/legion/extensions/uncertainty_tolerance/helpers/decision_spec.rb +125 -0
- data/spec/legion/extensions/uncertainty_tolerance/helpers/tolerance_engine_spec.rb +184 -0
- data/spec/legion/extensions/uncertainty_tolerance/runners/uncertainty_tolerance_spec.rb +157 -0
- data/spec/spec_helper.rb +20 -0
- metadata +76 -0
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,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,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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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: []
|