lex-resilience 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/lib/legion/extensions/resilience/client.rb +23 -0
- data/lib/legion/extensions/resilience/helpers/adversity_tracker.rb +126 -0
- data/lib/legion/extensions/resilience/helpers/constants.rb +75 -0
- data/lib/legion/extensions/resilience/helpers/resilience_model.rb +161 -0
- data/lib/legion/extensions/resilience/runners/resilience.rb +146 -0
- data/lib/legion/extensions/resilience/version.rb +9 -0
- data/lib/legion/extensions/resilience.rb +16 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c568cb863f7bce59fa4b9671135e36e5ac16142915c271f217e97f926810257c
|
|
4
|
+
data.tar.gz: b727773a253036e8ff144cade567ae6ade65933f760e0febf188a53242e8d0e1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e9ec2c68dfa1d5fd33a7f1160b2650c5d0d89e27354b8862e5cda540c94bd71a0f18682870583260f322c6c3c075311c856bc0902fece25f620d012046f17660
|
|
7
|
+
data.tar.gz: 3fca179ba62761d865ef34aa12473585990bb6a7456929eb3961c2beec2dd5ae6d4ff06d976b476e475d4804c91c51fec341881a0b4dcff9b5e55b113ac58947
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/resilience/helpers/constants'
|
|
4
|
+
require 'legion/extensions/resilience/helpers/adversity_tracker'
|
|
5
|
+
require 'legion/extensions/resilience/helpers/resilience_model'
|
|
6
|
+
require 'legion/extensions/resilience/runners/resilience'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Resilience
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::Resilience
|
|
13
|
+
|
|
14
|
+
attr_reader :adversity_tracker, :resilience_model
|
|
15
|
+
|
|
16
|
+
def initialize(adversity_tracker: nil, resilience_model: nil, **)
|
|
17
|
+
@adversity_tracker = adversity_tracker || Helpers::AdversityTracker.new
|
|
18
|
+
@resilience_model = resilience_model || Helpers::ResilienceModel.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Resilience
|
|
6
|
+
module Helpers
|
|
7
|
+
class AdversityTracker
|
|
8
|
+
attr_reader :active_adversities, :resolved_adversities, :consecutive_recoveries
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@active_adversities = []
|
|
12
|
+
@resolved_adversities = []
|
|
13
|
+
@consecutive_recoveries = 0
|
|
14
|
+
@adversity_counter = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(type:, severity:, context: {})
|
|
18
|
+
return nil unless Constants::ADVERSITY_TYPES.include?(type)
|
|
19
|
+
return nil unless Constants::SEVERITY_LEVELS.key?(severity)
|
|
20
|
+
|
|
21
|
+
@adversity_counter += 1
|
|
22
|
+
severity_config = Constants::SEVERITY_LEVELS[severity]
|
|
23
|
+
|
|
24
|
+
adversity = {
|
|
25
|
+
id: @adversity_counter,
|
|
26
|
+
type: type,
|
|
27
|
+
severity: severity,
|
|
28
|
+
impact: severity_config[:impact],
|
|
29
|
+
expected_ticks: severity_config[:recovery_ticks],
|
|
30
|
+
phase: :absorbing,
|
|
31
|
+
health_at_onset: 1.0,
|
|
32
|
+
current_health: 1.0 - severity_config[:impact],
|
|
33
|
+
ticks_elapsed: 0,
|
|
34
|
+
context: context,
|
|
35
|
+
registered_at: Time.now.utc
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@active_adversities << adversity
|
|
39
|
+
trim_active
|
|
40
|
+
adversity
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tick_recovery
|
|
44
|
+
@active_adversities.each do |adv|
|
|
45
|
+
adv[:ticks_elapsed] += 1
|
|
46
|
+
advance_phase(adv)
|
|
47
|
+
recover_health(adv)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
newly_resolved = @active_adversities.select { |a| a[:current_health] >= Constants::RECOVERY_THRESHOLD }
|
|
51
|
+
newly_resolved.each { |a| resolve(a) }
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
active_count: @active_adversities.size,
|
|
55
|
+
resolved_count: newly_resolved.size,
|
|
56
|
+
worst_health: worst_health
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def worst_health
|
|
61
|
+
return 1.0 if @active_adversities.empty?
|
|
62
|
+
|
|
63
|
+
@active_adversities.map { |a| a[:current_health] }.min
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def active_by_type
|
|
67
|
+
@active_adversities.group_by { |a| a[:type] }.transform_values(&:size)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def recovery_rate
|
|
71
|
+
total = @resolved_adversities.size
|
|
72
|
+
return 0.0 if total.zero?
|
|
73
|
+
|
|
74
|
+
on_time = @resolved_adversities.count { |a| a[:ticks_elapsed] <= a[:expected_ticks] }
|
|
75
|
+
on_time.to_f / total
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def average_recovery_speed
|
|
79
|
+
return 0.0 if @resolved_adversities.empty?
|
|
80
|
+
|
|
81
|
+
ratios = @resolved_adversities.map { |a| a[:ticks_elapsed].to_f / [a[:expected_ticks], 1].max }
|
|
82
|
+
ratios.sum / ratios.size.to_f
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def total_adversities
|
|
86
|
+
@active_adversities.size + @resolved_adversities.size
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def advance_phase(adv)
|
|
92
|
+
progress = adv[:current_health]
|
|
93
|
+
adv[:phase] = if progress < 0.3
|
|
94
|
+
:absorbing
|
|
95
|
+
elsif progress < 0.6
|
|
96
|
+
:adapting
|
|
97
|
+
elsif progress < Constants::RECOVERY_THRESHOLD
|
|
98
|
+
:recovering
|
|
99
|
+
else
|
|
100
|
+
:thriving
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def recover_health(adv)
|
|
105
|
+
recovery_rate = 1.0 / [adv[:expected_ticks], 1].max
|
|
106
|
+
adv[:current_health] = [adv[:current_health] + recovery_rate, 1.0].min
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve(adv)
|
|
110
|
+
adv[:phase] = :thriving
|
|
111
|
+
adv[:resolved_at] = Time.now.utc
|
|
112
|
+
@active_adversities.delete(adv)
|
|
113
|
+
@resolved_adversities << adv
|
|
114
|
+
@resolved_adversities.shift while @resolved_adversities.size > Constants::MAX_RESILIENCE_HISTORY
|
|
115
|
+
|
|
116
|
+
@consecutive_recoveries += 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def trim_active
|
|
120
|
+
@active_adversities.shift while @active_adversities.size > Constants::MAX_ACTIVE_ADVERSITIES
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Resilience
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# Adversity types the resilience system tracks
|
|
9
|
+
ADVERSITY_TYPES = %i[
|
|
10
|
+
prediction_failure
|
|
11
|
+
trust_violation
|
|
12
|
+
conflict_escalation
|
|
13
|
+
resource_depletion
|
|
14
|
+
communication_failure
|
|
15
|
+
goal_failure
|
|
16
|
+
emotional_shock
|
|
17
|
+
system_error
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Recovery phases (Masten's resilience model)
|
|
21
|
+
RECOVERY_PHASES = %i[
|
|
22
|
+
absorbing
|
|
23
|
+
adapting
|
|
24
|
+
recovering
|
|
25
|
+
thriving
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Resilience dimensions
|
|
29
|
+
DIMENSIONS = {
|
|
30
|
+
elasticity: { description: 'Speed of recovery to baseline', weight: 0.30 },
|
|
31
|
+
robustness: { description: 'Resistance to initial disruption', weight: 0.25 },
|
|
32
|
+
adaptability: { description: 'Capacity to adjust strategy', weight: 0.25 },
|
|
33
|
+
growth: { description: 'Ability to improve from adversity', weight: 0.20 }
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# EMA alpha for resilience dimension tracking
|
|
37
|
+
RESILIENCE_ALPHA = 0.08
|
|
38
|
+
|
|
39
|
+
# Growth bonus per successful recovery
|
|
40
|
+
GROWTH_INCREMENT = 0.02
|
|
41
|
+
|
|
42
|
+
# Maximum growth bonus (anti-fragile ceiling)
|
|
43
|
+
MAX_GROWTH_BONUS = 0.3
|
|
44
|
+
|
|
45
|
+
# Adversity severity levels
|
|
46
|
+
SEVERITY_LEVELS = {
|
|
47
|
+
minor: { impact: 0.1, recovery_ticks: 5 },
|
|
48
|
+
moderate: { impact: 0.3, recovery_ticks: 15 },
|
|
49
|
+
major: { impact: 0.5, recovery_ticks: 30 },
|
|
50
|
+
severe: { impact: 0.8, recovery_ticks: 60 },
|
|
51
|
+
critical: { impact: 1.0, recovery_ticks: 100 }
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Threshold for considering recovery complete
|
|
55
|
+
RECOVERY_THRESHOLD = 0.9
|
|
56
|
+
|
|
57
|
+
# Maximum active adversity events tracked
|
|
58
|
+
MAX_ACTIVE_ADVERSITIES = 20
|
|
59
|
+
|
|
60
|
+
# History cap
|
|
61
|
+
MAX_RESILIENCE_HISTORY = 200
|
|
62
|
+
|
|
63
|
+
# Fragility threshold — below this, the system is fragile
|
|
64
|
+
FRAGILITY_THRESHOLD = 0.3
|
|
65
|
+
|
|
66
|
+
# Anti-fragility threshold — above this, the system grows from stress
|
|
67
|
+
ANTIFRAGILITY_THRESHOLD = 0.7
|
|
68
|
+
|
|
69
|
+
# Consecutive recoveries needed to boost growth dimension
|
|
70
|
+
GROWTH_TRIGGER = 3
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Resilience
|
|
6
|
+
module Helpers
|
|
7
|
+
class ResilienceModel
|
|
8
|
+
attr_reader :dimensions, :growth_bonus, :history
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@dimensions = Constants::DIMENSIONS.keys.to_h { |d| [d, 0.5] }
|
|
12
|
+
@growth_bonus = 0.0
|
|
13
|
+
@history = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update_from_tracker(tracker)
|
|
17
|
+
update_elasticity(tracker)
|
|
18
|
+
update_robustness(tracker)
|
|
19
|
+
update_adaptability(tracker)
|
|
20
|
+
update_growth(tracker)
|
|
21
|
+
|
|
22
|
+
record_snapshot
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def composite_score
|
|
26
|
+
total = 0.0
|
|
27
|
+
Constants::DIMENSIONS.each do |dim, config|
|
|
28
|
+
total += (@dimensions[dim] + (dim == :growth ? @growth_bonus : 0.0)) * config[:weight]
|
|
29
|
+
end
|
|
30
|
+
total.clamp(0.0, 1.0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def classification
|
|
34
|
+
score = composite_score
|
|
35
|
+
if score >= Constants::ANTIFRAGILITY_THRESHOLD
|
|
36
|
+
:antifragile
|
|
37
|
+
elsif score >= 0.5
|
|
38
|
+
:resilient
|
|
39
|
+
elsif score >= Constants::FRAGILITY_THRESHOLD
|
|
40
|
+
:fragile
|
|
41
|
+
else
|
|
42
|
+
:brittle
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dimension_detail(name)
|
|
47
|
+
return nil unless @dimensions.key?(name)
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
name: name,
|
|
51
|
+
value: @dimensions[name].round(4),
|
|
52
|
+
config: Constants::DIMENSIONS[name],
|
|
53
|
+
trend: dimension_trend(name),
|
|
54
|
+
healthy: @dimensions[name] >= 0.5
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def trend
|
|
59
|
+
return :insufficient_data if @history.size < 5
|
|
60
|
+
|
|
61
|
+
recent = @history.last(10)
|
|
62
|
+
scores = recent.map { |h| h[:composite] }
|
|
63
|
+
first_half = scores[0...(scores.size / 2)]
|
|
64
|
+
second_half = scores[(scores.size / 2)..]
|
|
65
|
+
diff = mean(second_half) - mean(first_half)
|
|
66
|
+
|
|
67
|
+
if diff > 0.03
|
|
68
|
+
:strengthening
|
|
69
|
+
elsif diff < -0.03
|
|
70
|
+
:weakening
|
|
71
|
+
else
|
|
72
|
+
:stable
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{
|
|
78
|
+
dimensions: @dimensions.transform_values { |v| v.round(4) },
|
|
79
|
+
growth_bonus: @growth_bonus.round(4),
|
|
80
|
+
composite: composite_score.round(4),
|
|
81
|
+
class: classification,
|
|
82
|
+
trend: trend
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def update_elasticity(tracker)
|
|
89
|
+
speed = tracker.average_recovery_speed
|
|
90
|
+
signal = if speed.zero?
|
|
91
|
+
0.5
|
|
92
|
+
elsif speed <= 1.0
|
|
93
|
+
0.7 + ((1.0 - speed) * 0.3)
|
|
94
|
+
else
|
|
95
|
+
[0.3, 0.7 - ((speed - 1.0) * 0.2)].max
|
|
96
|
+
end
|
|
97
|
+
@dimensions[:elasticity] = ema(@dimensions[:elasticity], signal, Constants::RESILIENCE_ALPHA)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def update_robustness(tracker)
|
|
101
|
+
worst = tracker.worst_health
|
|
102
|
+
@dimensions[:robustness] = ema(@dimensions[:robustness], worst, Constants::RESILIENCE_ALPHA)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def update_adaptability(tracker)
|
|
106
|
+
rate = tracker.recovery_rate
|
|
107
|
+
signal = tracker.total_adversities.zero? ? 0.5 : rate
|
|
108
|
+
@dimensions[:adaptability] = ema(@dimensions[:adaptability], signal, Constants::RESILIENCE_ALPHA)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def update_growth(tracker)
|
|
112
|
+
if tracker.consecutive_recoveries >= Constants::GROWTH_TRIGGER
|
|
113
|
+
@growth_bonus = [@growth_bonus + Constants::GROWTH_INCREMENT, Constants::MAX_GROWTH_BONUS].min
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
growth_signal = tracker.total_adversities.zero? ? 0.5 : 0.5 + @growth_bonus
|
|
117
|
+
@dimensions[:growth] = ema(@dimensions[:growth], growth_signal, Constants::RESILIENCE_ALPHA)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dimension_trend(name)
|
|
121
|
+
return :insufficient_data if @history.size < 5
|
|
122
|
+
|
|
123
|
+
recent = @history.last(10)
|
|
124
|
+
values = recent.map { |h| h[:dimensions][name] }
|
|
125
|
+
first_half = values[0...(values.size / 2)]
|
|
126
|
+
second_half = values[(values.size / 2)..]
|
|
127
|
+
diff = mean(second_half) - mean(first_half)
|
|
128
|
+
|
|
129
|
+
if diff > 0.02
|
|
130
|
+
:improving
|
|
131
|
+
elsif diff < -0.02
|
|
132
|
+
:declining
|
|
133
|
+
else
|
|
134
|
+
:stable
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ema(current, observed, alpha)
|
|
139
|
+
(current * (1.0 - alpha)) + (observed * alpha)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def mean(values)
|
|
143
|
+
return 0.0 if values.empty?
|
|
144
|
+
|
|
145
|
+
values.sum / values.size.to_f
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_snapshot
|
|
149
|
+
@history << {
|
|
150
|
+
dimensions: @dimensions.dup,
|
|
151
|
+
composite: composite_score,
|
|
152
|
+
class: classification,
|
|
153
|
+
at: Time.now.utc
|
|
154
|
+
}
|
|
155
|
+
@history.shift while @history.size > Constants::MAX_RESILIENCE_HISTORY
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Resilience
|
|
6
|
+
module Runners
|
|
7
|
+
module Resilience
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def update_resilience(tick_results: {}, **)
|
|
12
|
+
detect_adversities(tick_results)
|
|
13
|
+
recovery = adversity_tracker.tick_recovery
|
|
14
|
+
resilience_model.update_from_tracker(adversity_tracker)
|
|
15
|
+
|
|
16
|
+
Legion::Logging.debug "[resilience] active=#{recovery[:active_count]} " \
|
|
17
|
+
"resolved=#{recovery[:resolved_count]} " \
|
|
18
|
+
"composite=#{resilience_model.composite_score.round(3)}"
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
active_adversities: recovery[:active_count],
|
|
22
|
+
resolved_this_tick: recovery[:resolved_count],
|
|
23
|
+
worst_health: recovery[:worst_health],
|
|
24
|
+
composite_score: resilience_model.composite_score.round(4),
|
|
25
|
+
classification: resilience_model.classification,
|
|
26
|
+
growth_bonus: resilience_model.growth_bonus.round(4)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def register_adversity(type:, severity:, context: {}, **)
|
|
31
|
+
adversity = adversity_tracker.register(type: type, severity: severity, context: context)
|
|
32
|
+
return { success: false, error: 'invalid type or severity' } unless adversity
|
|
33
|
+
|
|
34
|
+
Legion::Logging.info "[resilience] adversity registered: type=#{type} severity=#{severity}"
|
|
35
|
+
{ success: true, adversity: adversity }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resilience_status(**)
|
|
39
|
+
model_state = resilience_model.to_h
|
|
40
|
+
Legion::Logging.debug "[resilience] status: #{model_state[:class]} score=#{model_state[:composite]}"
|
|
41
|
+
|
|
42
|
+
model_state.merge(
|
|
43
|
+
active_adversities: adversity_tracker.active_adversities.size,
|
|
44
|
+
total_adversities: adversity_tracker.total_adversities,
|
|
45
|
+
consecutive_recoveries: adversity_tracker.consecutive_recoveries,
|
|
46
|
+
recovery_rate: adversity_tracker.recovery_rate.round(4)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def adversity_report(**)
|
|
51
|
+
Legion::Logging.debug '[resilience] adversity report'
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
active: adversity_tracker.active_adversities,
|
|
55
|
+
by_type: adversity_tracker.active_by_type,
|
|
56
|
+
total: adversity_tracker.total_adversities,
|
|
57
|
+
worst: adversity_tracker.worst_health.round(4),
|
|
58
|
+
avg_speed: adversity_tracker.average_recovery_speed.round(4)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dimension_detail(dimension:, **)
|
|
63
|
+
detail = resilience_model.dimension_detail(dimension.to_sym)
|
|
64
|
+
return { error: "unknown dimension: #{dimension}" } unless detail
|
|
65
|
+
|
|
66
|
+
Legion::Logging.debug "[resilience] dimension #{dimension}: #{detail[:value]}"
|
|
67
|
+
detail
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resilience_stats(**)
|
|
71
|
+
Legion::Logging.debug '[resilience] stats'
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
composite: resilience_model.composite_score.round(4),
|
|
75
|
+
classification: resilience_model.classification,
|
|
76
|
+
dimensions: resilience_model.dimensions.transform_values { |v| v.round(4) },
|
|
77
|
+
growth_bonus: resilience_model.growth_bonus.round(4),
|
|
78
|
+
trend: resilience_model.trend,
|
|
79
|
+
total_adversities: adversity_tracker.total_adversities,
|
|
80
|
+
active_adversities: adversity_tracker.active_adversities.size,
|
|
81
|
+
recovery_rate: adversity_tracker.recovery_rate.round(4),
|
|
82
|
+
consecutive_recoveries: adversity_tracker.consecutive_recoveries,
|
|
83
|
+
history_size: resilience_model.history.size
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def adversity_tracker
|
|
90
|
+
@adversity_tracker ||= Helpers::AdversityTracker.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def resilience_model
|
|
94
|
+
@resilience_model ||= Helpers::ResilienceModel.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def detect_adversities(tick_results)
|
|
98
|
+
detect_prediction_adversity(tick_results)
|
|
99
|
+
detect_trust_adversity(tick_results)
|
|
100
|
+
detect_conflict_adversity(tick_results)
|
|
101
|
+
detect_energy_adversity(tick_results)
|
|
102
|
+
detect_emotional_adversity(tick_results)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def detect_prediction_adversity(tick_results)
|
|
106
|
+
error_rate = tick_results.dig(:prediction_engine, :error_rate)
|
|
107
|
+
return unless error_rate && error_rate > 0.7
|
|
108
|
+
|
|
109
|
+
severity = error_rate > 0.9 ? :major : :moderate
|
|
110
|
+
adversity_tracker.register(type: :prediction_failure, severity: severity)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def detect_trust_adversity(tick_results)
|
|
114
|
+
violation = tick_results.dig(:trust, :violation)
|
|
115
|
+
return unless violation
|
|
116
|
+
|
|
117
|
+
adversity_tracker.register(type: :trust_violation, severity: :major)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def detect_conflict_adversity(tick_results)
|
|
121
|
+
conflict_severity = tick_results.dig(:conflict, :severity)
|
|
122
|
+
return unless conflict_severity && conflict_severity >= 3
|
|
123
|
+
|
|
124
|
+
severity = conflict_severity >= 4 ? :severe : :moderate
|
|
125
|
+
adversity_tracker.register(type: :conflict_escalation, severity: severity)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def detect_energy_adversity(tick_results)
|
|
129
|
+
energy = tick_results.dig(:fatigue, :energy)
|
|
130
|
+
return unless energy && energy < 0.2
|
|
131
|
+
|
|
132
|
+
severity = energy < 0.1 ? :major : :moderate
|
|
133
|
+
adversity_tracker.register(type: :resource_depletion, severity: severity)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def detect_emotional_adversity(tick_results)
|
|
137
|
+
arousal = tick_results.dig(:emotional_evaluation, :arousal)
|
|
138
|
+
return unless arousal && arousal > 0.9
|
|
139
|
+
|
|
140
|
+
adversity_tracker.register(type: :emotional_shock, severity: :moderate)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/resilience/version'
|
|
4
|
+
require 'legion/extensions/resilience/helpers/constants'
|
|
5
|
+
require 'legion/extensions/resilience/helpers/adversity_tracker'
|
|
6
|
+
require 'legion/extensions/resilience/helpers/resilience_model'
|
|
7
|
+
require 'legion/extensions/resilience/runners/resilience'
|
|
8
|
+
require 'legion/extensions/resilience/client'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Resilience
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-resilience
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
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 resilience as the capacity to recover from setbacks and grow stronger
|
|
27
|
+
from adversity
|
|
28
|
+
email:
|
|
29
|
+
- matt@legionIO.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/legion/extensions/resilience.rb
|
|
35
|
+
- lib/legion/extensions/resilience/client.rb
|
|
36
|
+
- lib/legion/extensions/resilience/helpers/adversity_tracker.rb
|
|
37
|
+
- lib/legion/extensions/resilience/helpers/constants.rb
|
|
38
|
+
- lib/legion/extensions/resilience/helpers/resilience_model.rb
|
|
39
|
+
- lib/legion/extensions/resilience/runners/resilience.rb
|
|
40
|
+
- lib/legion/extensions/resilience/version.rb
|
|
41
|
+
homepage: https://github.com/LegionIO/lex-resilience
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.4'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.6.9
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Anti-fragility and adversity recovery for LegionIO cognitive agents
|
|
63
|
+
test_files: []
|