lex-cognitive-weathering 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 +15 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/lex-cognitive-weathering.gemspec +29 -0
- data/lib/legion/extensions/cognitive_weathering/client.rb +20 -0
- data/lib/legion/extensions/cognitive_weathering/helpers/constants.rb +62 -0
- data/lib/legion/extensions/cognitive_weathering/helpers/stressor.rb +61 -0
- data/lib/legion/extensions/cognitive_weathering/helpers/weathering_engine.rb +121 -0
- data/lib/legion/extensions/cognitive_weathering/runners/cognitive_weathering.rb +70 -0
- data/lib/legion/extensions/cognitive_weathering/version.rb +9 -0
- data/lib/legion/extensions/cognitive_weathering.rb +15 -0
- data/spec/legion/extensions/cognitive_weathering/client_spec.rb +70 -0
- data/spec/legion/extensions/cognitive_weathering/helpers/constants_spec.rb +147 -0
- data/spec/legion/extensions/cognitive_weathering/helpers/stressor_spec.rb +161 -0
- data/spec/legion/extensions/cognitive_weathering/helpers/weathering_engine_spec.rb +242 -0
- data/spec/legion/extensions/cognitive_weathering/runners/cognitive_weathering_spec.rb +107 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: db91ee5a95c7738bea4c0f2f8c81a941e27277376864e4981340b12bee855604
|
|
4
|
+
data.tar.gz: 3872c8f28774524bcdda173da925369949b08337b17a729efb9758cf1f2ff8e7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d01d7ab9af6ad5db0f289c88b57f5d34fa862c032442d202b581eb0915590da52515191501a95d8afdf95ed1a84e508882475c61cd538af518197129f59c6cf7
|
|
7
|
+
data.tar.gz: b259fc75e57095227014081979a61f5d748ff2d3567af1a627f8ee5653cc2a99625df072a4627405a55dd6d0ca3b659d877e64886bf5f758febe4542abfaa39c
|
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec', '~> 3.13'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop', '~> 1.75', require: false
|
|
11
|
+
gem 'rubocop-rspec', require: false
|
|
12
|
+
gem 'simplecov'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Esity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# lex-cognitive-weathering
|
|
2
|
+
|
|
3
|
+
A LegionIO cognitive architecture extension that models long-term cumulative cognitive wear. Based on allostatic load theory: sustained demands erode integrity, but manageable challenges also build tempering — a resilience multiplier that allows tempered agents to exceed their baseline capacity.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Tracks cognitive **integrity** (0.0–1.0) and **tempering level** (0.0–1.0) through stressor events and recovery.
|
|
8
|
+
|
|
9
|
+
Each stressor has:
|
|
10
|
+
- A type (`:cognitive_overload`, `:emotional_strain`, `:decision_fatigue`, `:conflict_exposure`, `:uncertainty`, `:time_pressure`, `:monotony`, `:complexity`)
|
|
11
|
+
- Intensity (0.0–1.0) and duration (seconds)
|
|
12
|
+
- `cumulative_impact` = `intensity * (duration / 3600.0)`
|
|
13
|
+
|
|
14
|
+
Stressors with intensity <= 0.4 are **manageable** — they wear down integrity *and* build tempering. Overwhelming stressors (intensity >= 0.8) erode without building resilience.
|
|
15
|
+
|
|
16
|
+
**Effective capacity** = `integrity * (1 + tempering * 0.2)` — a tempered agent can exceed 1.0 base capacity (up to 1.2).
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require 'lex-cognitive-weathering'
|
|
22
|
+
|
|
23
|
+
client = Legion::Extensions::CognitiveWeathering::Client.new
|
|
24
|
+
|
|
25
|
+
# Apply an overwhelming stressor (erodes integrity, no tempering)
|
|
26
|
+
client.apply_stressor(
|
|
27
|
+
description: 'Sprint deadline pressure',
|
|
28
|
+
stressor_type: :time_pressure,
|
|
29
|
+
intensity: 0.8,
|
|
30
|
+
duration: 7200,
|
|
31
|
+
domain: 'engineering'
|
|
32
|
+
)
|
|
33
|
+
# => { integrity: 0.9984, tempering_level: 0.0, effective_capacity: 0.9984, fragile: false, ... }
|
|
34
|
+
|
|
35
|
+
# Apply a manageable stressor (erodes slightly, builds tempering)
|
|
36
|
+
client.apply_stressor(
|
|
37
|
+
description: 'Steady background complexity',
|
|
38
|
+
stressor_type: :complexity,
|
|
39
|
+
intensity: 0.3,
|
|
40
|
+
duration: 3600
|
|
41
|
+
)
|
|
42
|
+
# => { integrity: 0.9980, tempering_level: 0.0009, effective_capacity: 0.9982, ... }
|
|
43
|
+
|
|
44
|
+
# Check current state
|
|
45
|
+
client.integrity_status
|
|
46
|
+
# => { integrity: 0.9980, integrity_label: "pristine", tempering_level: 0.0009, weathering_label: "eroded", ... }
|
|
47
|
+
|
|
48
|
+
# Recover from wear (small increment)
|
|
49
|
+
client.recover(amount: 1.0)
|
|
50
|
+
# => { integrity: 0.9990, ... }
|
|
51
|
+
|
|
52
|
+
# Full rest (5x recovery rate)
|
|
53
|
+
client.rest(amount: 1.0)
|
|
54
|
+
# => { integrity: 0.9995, ... }
|
|
55
|
+
|
|
56
|
+
# Full weathering report
|
|
57
|
+
client.weathering_report
|
|
58
|
+
# => { integrity: 0.9995, integrity_label: "pristine", tempering_level: 0.0009, weathering_label: "eroded",
|
|
59
|
+
# effective_capacity: 0.9997, total_wear: 0.0021, total_recovery: 0.0015,
|
|
60
|
+
# stressor_count: 2, fragile: false, breaking: false, recent_stressors: [...] }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bundle install
|
|
67
|
+
bundle exec rspec
|
|
68
|
+
bundle exec rubocop
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/cognitive_weathering/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-cognitive-weathering'
|
|
7
|
+
spec.version = Legion::Extensions::CognitiveWeathering::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Cognitive Weathering'
|
|
12
|
+
spec.description = 'Models long-term cognitive wear from sustained workloads based on allostatic load theory'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-cognitive-weathering'
|
|
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-cognitive-weathering'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-weathering'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-weathering'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-weathering/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-cognitive-weathering.gemspec Gemfile LICENSE README.md]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_weathering/helpers/constants'
|
|
4
|
+
require 'legion/extensions/cognitive_weathering/helpers/stressor'
|
|
5
|
+
require 'legion/extensions/cognitive_weathering/helpers/weathering_engine'
|
|
6
|
+
require 'legion/extensions/cognitive_weathering/runners/cognitive_weathering'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module CognitiveWeathering
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::CognitiveWeathering
|
|
13
|
+
|
|
14
|
+
def initialize(**)
|
|
15
|
+
@weathering_engine = Helpers::WeatheringEngine.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveWeathering
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_STRESSORS = 200
|
|
9
|
+
MAX_EVENTS = 500
|
|
10
|
+
|
|
11
|
+
DEFAULT_INTEGRITY = 1.0
|
|
12
|
+
WEAR_RATE = 0.02
|
|
13
|
+
RECOVERY_RATE = 0.01
|
|
14
|
+
TEMPERING_RATE = 0.03
|
|
15
|
+
|
|
16
|
+
TEMPERING_THRESHOLD = 0.4
|
|
17
|
+
CRITICAL_INTEGRITY = 0.3
|
|
18
|
+
BREAKDOWN_INTEGRITY = 0.1
|
|
19
|
+
|
|
20
|
+
STRESSOR_TYPES = %i[
|
|
21
|
+
cognitive_overload
|
|
22
|
+
emotional_strain
|
|
23
|
+
decision_fatigue
|
|
24
|
+
conflict_exposure
|
|
25
|
+
uncertainty
|
|
26
|
+
time_pressure
|
|
27
|
+
monotony
|
|
28
|
+
complexity
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
INTEGRITY_LABELS = [
|
|
32
|
+
{ range: (0.8..1.0), label: 'pristine' },
|
|
33
|
+
{ range: (0.6...0.8), label: 'strong' },
|
|
34
|
+
{ range: (0.4...0.6), label: 'worn' },
|
|
35
|
+
{ range: (0.2...0.4), label: 'fragile' },
|
|
36
|
+
{ range: (0.0...0.2), label: 'breaking' }
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
WEATHERING_LABELS = [
|
|
40
|
+
{ range: (0.7..1.0), label: 'tempered' },
|
|
41
|
+
{ range: (0.5...0.7), label: 'resilient' },
|
|
42
|
+
{ range: (0.3...0.5), label: 'stable' },
|
|
43
|
+
{ range: (0.1...0.3), label: 'weathered' },
|
|
44
|
+
{ range: (0.0...0.1), label: 'eroded' }
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def integrity_label(integrity)
|
|
50
|
+
entry = INTEGRITY_LABELS.find { |e| e[:range].cover?(integrity) }
|
|
51
|
+
entry ? entry[:label] : 'breaking'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def weathering_label(tempering_level)
|
|
55
|
+
entry = WEATHERING_LABELS.find { |e| e[:range].cover?(tempering_level) }
|
|
56
|
+
entry ? entry[:label] : 'eroded'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module CognitiveWeathering
|
|
8
|
+
module Helpers
|
|
9
|
+
class Stressor
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :description, :stressor_type, :intensity, :duration, :domain, :recorded_at
|
|
13
|
+
|
|
14
|
+
def initialize(description:, stressor_type:, intensity:, duration:, domain: nil)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@description = description
|
|
17
|
+
@stressor_type = validate_type(stressor_type)
|
|
18
|
+
@intensity = intensity.clamp(0.0, 1.0)
|
|
19
|
+
@duration = [duration.to_f, 0.0].max
|
|
20
|
+
@domain = domain
|
|
21
|
+
@recorded_at = Time.now.utc
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cumulative_impact
|
|
25
|
+
(intensity * (duration / 3600.0)).clamp(0.0, 1.0).round(10)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def manageable?
|
|
29
|
+
intensity <= Constants::TEMPERING_THRESHOLD
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def overwhelming?
|
|
33
|
+
intensity >= 0.8
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
id: id,
|
|
39
|
+
description: description,
|
|
40
|
+
stressor_type: stressor_type,
|
|
41
|
+
intensity: intensity,
|
|
42
|
+
duration: duration,
|
|
43
|
+
domain: domain,
|
|
44
|
+
cumulative_impact: cumulative_impact,
|
|
45
|
+
manageable: manageable?,
|
|
46
|
+
overwhelming: overwhelming?,
|
|
47
|
+
recorded_at: recorded_at.iso8601
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_type(type)
|
|
54
|
+
sym = type.to_sym
|
|
55
|
+
Constants::STRESSOR_TYPES.include?(sym) ? sym : :cognitive_overload
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveWeathering
|
|
6
|
+
module Helpers
|
|
7
|
+
class WeatheringEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :integrity, :tempering_level, :total_wear, :total_recovery
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@stressors = []
|
|
14
|
+
@events = []
|
|
15
|
+
@integrity = Constants::DEFAULT_INTEGRITY
|
|
16
|
+
@tempering_level = 0.0
|
|
17
|
+
@total_wear = 0.0
|
|
18
|
+
@total_recovery = 0.0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply_stressor(stressor)
|
|
22
|
+
prune_stressors
|
|
23
|
+
@stressors << stressor
|
|
24
|
+
|
|
25
|
+
record_event(:stressor_applied, stressor.to_h)
|
|
26
|
+
wear!(stressor.cumulative_impact)
|
|
27
|
+
temper!(stressor.cumulative_impact * Constants::TEMPERING_RATE) if stressor.manageable?
|
|
28
|
+
|
|
29
|
+
to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def recover!(amount)
|
|
33
|
+
amount = amount.clamp(0.0, 1.0)
|
|
34
|
+
delta = [amount * Constants::RECOVERY_RATE, 1.0 - @integrity].min
|
|
35
|
+
@integrity = (@integrity + delta).clamp(0.0, 1.0).round(10)
|
|
36
|
+
@total_recovery = (@total_recovery + delta).round(10)
|
|
37
|
+
record_event(:recovery, { amount: amount, delta: delta, integrity: @integrity })
|
|
38
|
+
to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rest!(amount = 1.0)
|
|
42
|
+
amount = amount.clamp(0.0, 1.0)
|
|
43
|
+
delta = [amount * (Constants::RECOVERY_RATE * 5.0), 1.0 - @integrity].min
|
|
44
|
+
@integrity = (@integrity + delta).clamp(0.0, 1.0).round(10)
|
|
45
|
+
@total_recovery = (@total_recovery + delta).round(10)
|
|
46
|
+
record_event(:rest, { amount: amount, delta: delta, integrity: @integrity })
|
|
47
|
+
to_h
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def effective_capacity
|
|
51
|
+
(@integrity * (1.0 + (@tempering_level * 0.2))).clamp(0.0, 1.2).round(10)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fragile?
|
|
55
|
+
@integrity <= Constants::CRITICAL_INTEGRITY
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def breaking?
|
|
59
|
+
@integrity <= Constants::BREAKDOWN_INTEGRITY
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stressor_count
|
|
63
|
+
@stressors.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def weathering_report
|
|
67
|
+
{
|
|
68
|
+
integrity: @integrity.round(10),
|
|
69
|
+
integrity_label: Constants.integrity_label(@integrity),
|
|
70
|
+
tempering_level: @tempering_level.round(10),
|
|
71
|
+
weathering_label: Constants.weathering_label(@tempering_level),
|
|
72
|
+
effective_capacity: effective_capacity,
|
|
73
|
+
total_wear: @total_wear.round(10),
|
|
74
|
+
total_recovery: @total_recovery.round(10),
|
|
75
|
+
stressor_count: @stressors.size,
|
|
76
|
+
fragile: fragile?,
|
|
77
|
+
breaking: breaking?,
|
|
78
|
+
recent_stressors: @stressors.last(5).map(&:to_h)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_h
|
|
83
|
+
{
|
|
84
|
+
integrity: @integrity.round(10),
|
|
85
|
+
tempering_level: @tempering_level.round(10),
|
|
86
|
+
effective_capacity: effective_capacity,
|
|
87
|
+
fragile: fragile?,
|
|
88
|
+
breaking: breaking?
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def wear!(amount)
|
|
95
|
+
delta = (amount * Constants::WEAR_RATE).clamp(0.0, @integrity)
|
|
96
|
+
@integrity = (@integrity - delta).clamp(0.0, 1.0).round(10)
|
|
97
|
+
@total_wear = (@total_wear + delta).round(10)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def temper!(amount)
|
|
101
|
+
delta = amount.clamp(0.0, 1.0 - @tempering_level)
|
|
102
|
+
@tempering_level = (@tempering_level + delta).clamp(0.0, 1.0).round(10)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def record_event(type, data)
|
|
106
|
+
prune_events
|
|
107
|
+
@events << { type: type, data: data, timestamp: Time.now.utc.iso8601 }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def prune_stressors
|
|
111
|
+
@stressors.shift while @stressors.size >= Constants::MAX_STRESSORS
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def prune_events
|
|
115
|
+
@events.shift while @events.size >= Constants::MAX_EVENTS
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveWeathering
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveWeathering
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def apply_stressor(description:, stressor_type: :cognitive_overload, intensity: 0.5,
|
|
12
|
+
duration: 3600, domain: nil, **)
|
|
13
|
+
stressor = Helpers::Stressor.new(
|
|
14
|
+
description: description,
|
|
15
|
+
stressor_type: stressor_type,
|
|
16
|
+
intensity: intensity,
|
|
17
|
+
duration: duration,
|
|
18
|
+
domain: domain
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
result = weathering_engine.apply_stressor(stressor)
|
|
22
|
+
Legion::Logging.debug "[cognitive_weathering] stressor applied: type=#{stressor_type} " \
|
|
23
|
+
"intensity=#{intensity} impact=#{stressor.cumulative_impact.round(4)} " \
|
|
24
|
+
"integrity=#{result[:integrity].round(4)} fragile=#{result[:fragile]}"
|
|
25
|
+
result.merge(stressor: stressor.to_h)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def recover(amount: 1.0, **)
|
|
29
|
+
result = weathering_engine.recover!(amount)
|
|
30
|
+
Legion::Logging.debug "[cognitive_weathering] recovery: amount=#{amount} integrity=#{result[:integrity].round(4)}"
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rest(amount: 1.0, **)
|
|
35
|
+
result = weathering_engine.rest!(amount)
|
|
36
|
+
Legion::Logging.debug "[cognitive_weathering] rest: amount=#{amount} integrity=#{result[:integrity].round(4)}"
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def weathering_report(**)
|
|
41
|
+
report = weathering_engine.weathering_report
|
|
42
|
+
Legion::Logging.debug "[cognitive_weathering] report: integrity=#{report[:integrity_label]} " \
|
|
43
|
+
"capacity=#{report[:effective_capacity].round(4)} " \
|
|
44
|
+
"stressors=#{report[:stressor_count]}"
|
|
45
|
+
report
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def integrity_status(**)
|
|
49
|
+
engine = weathering_engine
|
|
50
|
+
{
|
|
51
|
+
integrity: engine.integrity.round(10),
|
|
52
|
+
integrity_label: Helpers::Constants.integrity_label(engine.integrity),
|
|
53
|
+
tempering_level: engine.tempering_level.round(10),
|
|
54
|
+
weathering_label: Helpers::Constants.weathering_label(engine.tempering_level),
|
|
55
|
+
effective_capacity: engine.effective_capacity,
|
|
56
|
+
fragile: engine.fragile?,
|
|
57
|
+
breaking: engine.breaking?
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def weathering_engine
|
|
64
|
+
@weathering_engine ||= Helpers::WeatheringEngine.new
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_weathering/version'
|
|
4
|
+
require 'legion/extensions/cognitive_weathering/helpers/constants'
|
|
5
|
+
require 'legion/extensions/cognitive_weathering/helpers/stressor'
|
|
6
|
+
require 'legion/extensions/cognitive_weathering/helpers/weathering_engine'
|
|
7
|
+
require 'legion/extensions/cognitive_weathering/runners/cognitive_weathering'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module CognitiveWeathering
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_weathering/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveWeathering::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to apply_stressor' do
|
|
9
|
+
expect(client).to respond_to(:apply_stressor)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'responds to recover' do
|
|
13
|
+
expect(client).to respond_to(:recover)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'responds to rest' do
|
|
17
|
+
expect(client).to respond_to(:rest)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'responds to weathering_report' do
|
|
21
|
+
expect(client).to respond_to(:weathering_report)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'responds to integrity_status' do
|
|
25
|
+
expect(client).to respond_to(:integrity_status)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'starts with pristine integrity' do
|
|
29
|
+
status = client.integrity_status
|
|
30
|
+
expect(status[:integrity_label]).to eq('pristine')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'round-trips a full weathering cycle' do
|
|
34
|
+
client.apply_stressor(description: 'Sprint overload', stressor_type: :time_pressure,
|
|
35
|
+
intensity: 0.85, duration: 7200)
|
|
36
|
+
client.apply_stressor(description: 'Repeated decisions', stressor_type: :decision_fatigue,
|
|
37
|
+
intensity: 0.7, duration: 3600)
|
|
38
|
+
client.apply_stressor(description: 'Mild task', stressor_type: :monotony,
|
|
39
|
+
intensity: 0.2, duration: 1800)
|
|
40
|
+
|
|
41
|
+
report = client.weathering_report
|
|
42
|
+
expect(report[:stressor_count]).to eq(3)
|
|
43
|
+
expect(report[:integrity]).to be < 1.0
|
|
44
|
+
expect(report[:tempering_level]).to be > 0.0
|
|
45
|
+
|
|
46
|
+
client.rest(amount: 1.0)
|
|
47
|
+
after_rest = client.integrity_status
|
|
48
|
+
expect(after_rest[:integrity]).to be > report[:integrity]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'effective_capacity increases with tempering' do
|
|
52
|
+
20.times do
|
|
53
|
+
client.apply_stressor(description: 'Manageable', stressor_type: :complexity,
|
|
54
|
+
intensity: 0.3, duration: 1800)
|
|
55
|
+
end
|
|
56
|
+
status = client.integrity_status
|
|
57
|
+
expect(status[:tempering_level]).to be > 0.0
|
|
58
|
+
expect(status[:effective_capacity]).to be > 0.0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'each client instance has independent state' do
|
|
62
|
+
client_a = described_class.new
|
|
63
|
+
client_b = described_class.new
|
|
64
|
+
|
|
65
|
+
client_a.apply_stressor(description: 'only a', intensity: 0.9, duration: 7200)
|
|
66
|
+
|
|
67
|
+
expect(client_a.integrity_status[:integrity]).to be < 1.0
|
|
68
|
+
expect(client_b.integrity_status[:integrity]).to eq(1.0)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveWeathering::Helpers::Constants do
|
|
4
|
+
describe 'numeric constants' do
|
|
5
|
+
it 'defines MAX_STRESSORS' do
|
|
6
|
+
expect(described_class::MAX_STRESSORS).to eq(200)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'defines MAX_EVENTS' do
|
|
10
|
+
expect(described_class::MAX_EVENTS).to eq(500)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'defines DEFAULT_INTEGRITY as 1.0' do
|
|
14
|
+
expect(described_class::DEFAULT_INTEGRITY).to eq(1.0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'defines WEAR_RATE' do
|
|
18
|
+
expect(described_class::WEAR_RATE).to eq(0.02)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'defines RECOVERY_RATE' do
|
|
22
|
+
expect(described_class::RECOVERY_RATE).to eq(0.01)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'defines TEMPERING_RATE' do
|
|
26
|
+
expect(described_class::TEMPERING_RATE).to eq(0.03)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'defines TEMPERING_THRESHOLD' do
|
|
30
|
+
expect(described_class::TEMPERING_THRESHOLD).to eq(0.4)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'defines CRITICAL_INTEGRITY' do
|
|
34
|
+
expect(described_class::CRITICAL_INTEGRITY).to eq(0.3)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'defines BREAKDOWN_INTEGRITY' do
|
|
38
|
+
expect(described_class::BREAKDOWN_INTEGRITY).to eq(0.1)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'STRESSOR_TYPES' do
|
|
43
|
+
it 'contains all 8 stressor types' do
|
|
44
|
+
expect(described_class::STRESSOR_TYPES.size).to eq(8)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'includes :cognitive_overload' do
|
|
48
|
+
expect(described_class::STRESSOR_TYPES).to include(:cognitive_overload)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'includes :emotional_strain' do
|
|
52
|
+
expect(described_class::STRESSOR_TYPES).to include(:emotional_strain)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'includes :decision_fatigue' do
|
|
56
|
+
expect(described_class::STRESSOR_TYPES).to include(:decision_fatigue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'includes :conflict_exposure' do
|
|
60
|
+
expect(described_class::STRESSOR_TYPES).to include(:conflict_exposure)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'includes :uncertainty' do
|
|
64
|
+
expect(described_class::STRESSOR_TYPES).to include(:uncertainty)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'includes :time_pressure' do
|
|
68
|
+
expect(described_class::STRESSOR_TYPES).to include(:time_pressure)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'includes :monotony' do
|
|
72
|
+
expect(described_class::STRESSOR_TYPES).to include(:monotony)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'includes :complexity' do
|
|
76
|
+
expect(described_class::STRESSOR_TYPES).to include(:complexity)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'is frozen' do
|
|
80
|
+
expect(described_class::STRESSOR_TYPES).to be_frozen
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '.integrity_label' do
|
|
85
|
+
it 'returns pristine for 1.0' do
|
|
86
|
+
expect(described_class.integrity_label(1.0)).to eq('pristine')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns pristine for 0.8' do
|
|
90
|
+
expect(described_class.integrity_label(0.8)).to eq('pristine')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns strong for 0.7' do
|
|
94
|
+
expect(described_class.integrity_label(0.7)).to eq('strong')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns strong for 0.6' do
|
|
98
|
+
expect(described_class.integrity_label(0.6)).to eq('strong')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns worn for 0.5' do
|
|
102
|
+
expect(described_class.integrity_label(0.5)).to eq('worn')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'returns fragile for 0.25' do
|
|
106
|
+
expect(described_class.integrity_label(0.25)).to eq('fragile')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'returns breaking for 0.1' do
|
|
110
|
+
expect(described_class.integrity_label(0.1)).to eq('breaking')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'returns breaking for 0.0' do
|
|
114
|
+
expect(described_class.integrity_label(0.0)).to eq('breaking')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '.weathering_label' do
|
|
119
|
+
it 'returns tempered for 0.9' do
|
|
120
|
+
expect(described_class.weathering_label(0.9)).to eq('tempered')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'returns tempered for 0.7' do
|
|
124
|
+
expect(described_class.weathering_label(0.7)).to eq('tempered')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns resilient for 0.6' do
|
|
128
|
+
expect(described_class.weathering_label(0.6)).to eq('resilient')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns stable for 0.4' do
|
|
132
|
+
expect(described_class.weathering_label(0.4)).to eq('stable')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'returns weathered for 0.2' do
|
|
136
|
+
expect(described_class.weathering_label(0.2)).to eq('weathered')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns eroded for 0.05' do
|
|
140
|
+
expect(described_class.weathering_label(0.05)).to eq('eroded')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'returns eroded for 0.0' do
|
|
144
|
+
expect(described_class.weathering_label(0.0)).to eq('eroded')
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveWeathering::Helpers::Stressor do
|
|
4
|
+
let(:stressor) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
description: 'Heavy parallel task load',
|
|
7
|
+
stressor_type: :cognitive_overload,
|
|
8
|
+
intensity: 0.7,
|
|
9
|
+
duration: 3600,
|
|
10
|
+
domain: 'work'
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'assigns a UUID id' do
|
|
16
|
+
expect(stressor.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'assigns description' do
|
|
20
|
+
expect(stressor.description).to eq('Heavy parallel task load')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'assigns stressor_type' do
|
|
24
|
+
expect(stressor.stressor_type).to eq(:cognitive_overload)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'assigns intensity' do
|
|
28
|
+
expect(stressor.intensity).to eq(0.7)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'assigns duration' do
|
|
32
|
+
expect(stressor.duration).to eq(3600)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'assigns domain' do
|
|
36
|
+
expect(stressor.domain).to eq('work')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'records a timestamp' do
|
|
40
|
+
expect(stressor.recorded_at).to be_a(Time)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'clamps intensity above 1.0 to 1.0' do
|
|
44
|
+
s = described_class.new(description: 'test', stressor_type: :monotony, intensity: 1.5, duration: 60)
|
|
45
|
+
expect(s.intensity).to eq(1.0)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'clamps intensity below 0.0 to 0.0' do
|
|
49
|
+
s = described_class.new(description: 'test', stressor_type: :monotony, intensity: -0.3, duration: 60)
|
|
50
|
+
expect(s.intensity).to eq(0.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'defaults domain to nil when not provided' do
|
|
54
|
+
s = described_class.new(description: 'test', stressor_type: :complexity, intensity: 0.5, duration: 60)
|
|
55
|
+
expect(s.domain).to be_nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'falls back to :cognitive_overload for unknown stressor type' do
|
|
59
|
+
s = described_class.new(description: 'test', stressor_type: :banana, intensity: 0.5, duration: 60)
|
|
60
|
+
expect(s.stressor_type).to eq(:cognitive_overload)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'accepts string stressor type and converts to symbol' do
|
|
64
|
+
s = described_class.new(description: 'test', stressor_type: 'monotony', intensity: 0.5, duration: 60)
|
|
65
|
+
expect(s.stressor_type).to eq(:monotony)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'floors negative duration to 0' do
|
|
69
|
+
s = described_class.new(description: 'test', stressor_type: :uncertainty, intensity: 0.3, duration: -100)
|
|
70
|
+
expect(s.duration).to eq(0.0)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#cumulative_impact' do
|
|
75
|
+
it 'computes intensity * (duration / 3600)' do
|
|
76
|
+
expect(stressor.cumulative_impact).to be_within(0.0001).of(0.7)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'rounds to 10 decimal places' do
|
|
80
|
+
expect(stressor.cumulative_impact.to_s).to match(/\A\d+\.\d+\z/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'clamps at 1.0 for very long durations' do
|
|
84
|
+
s = described_class.new(description: 'long', stressor_type: :time_pressure, intensity: 1.0, duration: 100_000)
|
|
85
|
+
expect(s.cumulative_impact).to eq(1.0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns 0 for zero duration' do
|
|
89
|
+
s = described_class.new(description: 'instant', stressor_type: :uncertainty, intensity: 0.9, duration: 0)
|
|
90
|
+
expect(s.cumulative_impact).to eq(0.0)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#manageable?' do
|
|
95
|
+
it 'returns true when intensity is at or below TEMPERING_THRESHOLD' do
|
|
96
|
+
s = described_class.new(description: 'mild', stressor_type: :monotony, intensity: 0.4, duration: 60)
|
|
97
|
+
expect(s.manageable?).to be(true)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns false when intensity exceeds TEMPERING_THRESHOLD' do
|
|
101
|
+
s = described_class.new(description: 'heavy', stressor_type: :cognitive_overload, intensity: 0.6, duration: 60)
|
|
102
|
+
expect(s.manageable?).to be(false)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#overwhelming?' do
|
|
107
|
+
it 'returns true when intensity >= 0.8' do
|
|
108
|
+
s = described_class.new(description: 'extreme', stressor_type: :conflict_exposure, intensity: 0.9, duration: 60)
|
|
109
|
+
expect(s.overwhelming?).to be(true)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns false when intensity < 0.8' do
|
|
113
|
+
s = described_class.new(description: 'moderate', stressor_type: :complexity, intensity: 0.7, duration: 60)
|
|
114
|
+
expect(s.overwhelming?).to be(false)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#to_h' do
|
|
119
|
+
subject(:hash) { stressor.to_h }
|
|
120
|
+
|
|
121
|
+
it 'includes id' do
|
|
122
|
+
expect(hash[:id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'includes description' do
|
|
126
|
+
expect(hash[:description]).to eq('Heavy parallel task load')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'includes stressor_type' do
|
|
130
|
+
expect(hash[:stressor_type]).to eq(:cognitive_overload)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'includes intensity' do
|
|
134
|
+
expect(hash[:intensity]).to eq(0.7)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'includes duration' do
|
|
138
|
+
expect(hash[:duration]).to eq(3600)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'includes domain' do
|
|
142
|
+
expect(hash[:domain]).to eq('work')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'includes cumulative_impact' do
|
|
146
|
+
expect(hash[:cumulative_impact]).to be_a(Float)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'includes manageable flag' do
|
|
150
|
+
expect(hash[:manageable]).to be(false)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'includes overwhelming flag' do
|
|
154
|
+
expect(hash[:overwhelming]).to be(false)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'includes recorded_at as ISO8601 string' do
|
|
158
|
+
expect(hash[:recorded_at]).to match(/\d{4}-\d{2}-\d{2}T/)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveWeathering::Helpers::WeatheringEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:mild_stressor) do
|
|
7
|
+
Legion::Extensions::CognitiveWeathering::Helpers::Stressor.new(
|
|
8
|
+
description: 'Mild task',
|
|
9
|
+
stressor_type: :monotony,
|
|
10
|
+
intensity: 0.3,
|
|
11
|
+
duration: 1800
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:heavy_stressor) do
|
|
16
|
+
Legion::Extensions::CognitiveWeathering::Helpers::Stressor.new(
|
|
17
|
+
description: 'Extreme overload',
|
|
18
|
+
stressor_type: :cognitive_overload,
|
|
19
|
+
intensity: 0.9,
|
|
20
|
+
duration: 7200
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#initialize' do
|
|
25
|
+
it 'starts with full integrity' do
|
|
26
|
+
expect(engine.integrity).to eq(1.0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'starts with zero tempering_level' do
|
|
30
|
+
expect(engine.tempering_level).to eq(0.0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'starts with zero total_wear' do
|
|
34
|
+
expect(engine.total_wear).to eq(0.0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'starts with zero total_recovery' do
|
|
38
|
+
expect(engine.total_recovery).to eq(0.0)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#apply_stressor' do
|
|
43
|
+
it 'reduces integrity after a heavy stressor' do
|
|
44
|
+
engine.apply_stressor(heavy_stressor)
|
|
45
|
+
expect(engine.integrity).to be < 1.0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'increases total_wear' do
|
|
49
|
+
engine.apply_stressor(heavy_stressor)
|
|
50
|
+
expect(engine.total_wear).to be > 0.0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'does not increase tempering for overwhelming stressor' do
|
|
54
|
+
engine.apply_stressor(heavy_stressor)
|
|
55
|
+
expect(engine.tempering_level).to eq(0.0)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'increases tempering for manageable stressor' do
|
|
59
|
+
engine.apply_stressor(mild_stressor)
|
|
60
|
+
expect(engine.tempering_level).to be > 0.0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns a hash with integrity key' do
|
|
64
|
+
result = engine.apply_stressor(heavy_stressor)
|
|
65
|
+
expect(result).to have_key(:integrity)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns a hash with breaking key' do
|
|
69
|
+
result = engine.apply_stressor(heavy_stressor)
|
|
70
|
+
expect(result).to have_key(:breaking)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'increments stressor_count' do
|
|
74
|
+
engine.apply_stressor(mild_stressor)
|
|
75
|
+
expect(engine.stressor_count).to eq(1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'wear scales with cumulative impact' do
|
|
79
|
+
small = Legion::Extensions::CognitiveWeathering::Helpers::Stressor.new(
|
|
80
|
+
description: 'tiny', stressor_type: :monotony, intensity: 0.1, duration: 60
|
|
81
|
+
)
|
|
82
|
+
large = Legion::Extensions::CognitiveWeathering::Helpers::Stressor.new(
|
|
83
|
+
description: 'large', stressor_type: :cognitive_overload, intensity: 0.9, duration: 7200
|
|
84
|
+
)
|
|
85
|
+
engine_a = described_class.new
|
|
86
|
+
engine_b = described_class.new
|
|
87
|
+
engine_a.apply_stressor(small)
|
|
88
|
+
engine_b.apply_stressor(large)
|
|
89
|
+
expect(engine_b.total_wear).to be > engine_a.total_wear
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#recover!' do
|
|
94
|
+
before { engine.apply_stressor(heavy_stressor) }
|
|
95
|
+
|
|
96
|
+
it 'increases integrity' do
|
|
97
|
+
before = engine.integrity
|
|
98
|
+
engine.recover!(1.0)
|
|
99
|
+
expect(engine.integrity).to be > before
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'increases total_recovery' do
|
|
103
|
+
engine.recover!(1.0)
|
|
104
|
+
expect(engine.total_recovery).to be > 0.0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'returns a hash with integrity' do
|
|
108
|
+
result = engine.recover!(1.0)
|
|
109
|
+
expect(result).to have_key(:integrity)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'clamps amount above 1.0' do
|
|
113
|
+
expect { engine.recover!(5.0) }.not_to raise_error
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'does not exceed 1.0 integrity' do
|
|
117
|
+
10.times { engine.recover!(1.0) }
|
|
118
|
+
expect(engine.integrity).to be <= 1.0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#rest!' do
|
|
123
|
+
before { 5.times { engine.apply_stressor(heavy_stressor) } }
|
|
124
|
+
|
|
125
|
+
it 'increases integrity more than recover' do
|
|
126
|
+
engine_a = described_class.new
|
|
127
|
+
engine_b = described_class.new
|
|
128
|
+
5.times { engine_a.apply_stressor(heavy_stressor) }
|
|
129
|
+
5.times { engine_b.apply_stressor(heavy_stressor) }
|
|
130
|
+
integrity_before = engine_a.integrity
|
|
131
|
+
|
|
132
|
+
engine_a.recover!(1.0)
|
|
133
|
+
engine_b.rest!(1.0)
|
|
134
|
+
|
|
135
|
+
recovery_delta = engine_a.integrity - integrity_before
|
|
136
|
+
rest_delta = engine_b.integrity - integrity_before
|
|
137
|
+
expect(rest_delta).to be > recovery_delta
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'does not exceed 1.0 integrity' do
|
|
141
|
+
10.times { engine.rest!(1.0) }
|
|
142
|
+
expect(engine.integrity).to be <= 1.0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'increases total_recovery' do
|
|
146
|
+
engine.rest!(1.0)
|
|
147
|
+
expect(engine.total_recovery).to be > 0.0
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe '#effective_capacity' do
|
|
152
|
+
it 'starts at 1.0 with no tempering' do
|
|
153
|
+
expect(engine.effective_capacity).to be_within(0.001).of(1.0)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'increases above base when tempering is present' do
|
|
157
|
+
20.times { engine.apply_stressor(mild_stressor) }
|
|
158
|
+
expect(engine.tempering_level).to be > 0.0
|
|
159
|
+
expect(engine.effective_capacity).to be > (engine.integrity * 1.0)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'is capped at 1.2' do
|
|
163
|
+
100.times { engine.apply_stressor(mild_stressor) }
|
|
164
|
+
expect(engine.effective_capacity).to be <= 1.2
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'decreases as integrity drops' do
|
|
168
|
+
base = engine.effective_capacity
|
|
169
|
+
10.times { engine.apply_stressor(heavy_stressor) }
|
|
170
|
+
expect(engine.effective_capacity).to be < base
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe '#fragile?' do
|
|
175
|
+
it 'returns false at full integrity' do
|
|
176
|
+
expect(engine.fragile?).to be(false)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'returns true when integrity is at or below CRITICAL_INTEGRITY' do
|
|
180
|
+
100.times { engine.apply_stressor(heavy_stressor) }
|
|
181
|
+
expect(engine.fragile?).to be(true)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#breaking?' do
|
|
186
|
+
it 'returns false at full integrity' do
|
|
187
|
+
expect(engine.breaking?).to be(false)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'returns true when integrity is at or below BREAKDOWN_INTEGRITY' do
|
|
191
|
+
500.times { engine.apply_stressor(heavy_stressor) }
|
|
192
|
+
expect(engine.breaking?).to be(true)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe '#weathering_report' do
|
|
197
|
+
it 'returns all expected keys' do
|
|
198
|
+
report = engine.weathering_report
|
|
199
|
+
expect(report).to have_key(:integrity)
|
|
200
|
+
expect(report).to have_key(:integrity_label)
|
|
201
|
+
expect(report).to have_key(:tempering_level)
|
|
202
|
+
expect(report).to have_key(:weathering_label)
|
|
203
|
+
expect(report).to have_key(:effective_capacity)
|
|
204
|
+
expect(report).to have_key(:total_wear)
|
|
205
|
+
expect(report).to have_key(:total_recovery)
|
|
206
|
+
expect(report).to have_key(:stressor_count)
|
|
207
|
+
expect(report).to have_key(:fragile)
|
|
208
|
+
expect(report).to have_key(:breaking)
|
|
209
|
+
expect(report).to have_key(:recent_stressors)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'includes integrity_label as a string' do
|
|
213
|
+
expect(engine.weathering_report[:integrity_label]).to eq('pristine')
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'caps recent_stressors at 5' do
|
|
217
|
+
10.times { engine.apply_stressor(mild_stressor) }
|
|
218
|
+
expect(engine.weathering_report[:recent_stressors].size).to eq(5)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe '#to_h' do
|
|
223
|
+
it 'includes integrity, tempering_level, effective_capacity, fragile, breaking' do
|
|
224
|
+
h = engine.to_h
|
|
225
|
+
expect(h).to have_key(:integrity)
|
|
226
|
+
expect(h).to have_key(:tempering_level)
|
|
227
|
+
expect(h).to have_key(:effective_capacity)
|
|
228
|
+
expect(h).to have_key(:fragile)
|
|
229
|
+
expect(h).to have_key(:breaking)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
describe 'MAX_STRESSORS pruning' do
|
|
234
|
+
it 'does not exceed MAX_STRESSORS in memory' do
|
|
235
|
+
constants = Legion::Extensions::CognitiveWeathering::Helpers::Constants
|
|
236
|
+
(constants::MAX_STRESSORS + 10).times do
|
|
237
|
+
engine.apply_stressor(mild_stressor)
|
|
238
|
+
end
|
|
239
|
+
expect(engine.stressor_count).to be <= constants::MAX_STRESSORS
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_weathering/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveWeathering::Runners::CognitiveWeathering do
|
|
6
|
+
let(:client) { Legion::Extensions::CognitiveWeathering::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#apply_stressor' do
|
|
9
|
+
it 'returns a hash with integrity' do
|
|
10
|
+
result = client.apply_stressor(description: 'Heavy load', stressor_type: :cognitive_overload,
|
|
11
|
+
intensity: 0.8, duration: 3600)
|
|
12
|
+
expect(result).to have_key(:integrity)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'includes stressor details in result' do
|
|
16
|
+
result = client.apply_stressor(description: 'Test stressor', stressor_type: :monotony,
|
|
17
|
+
intensity: 0.3, duration: 1800)
|
|
18
|
+
expect(result[:stressor][:description]).to eq('Test stressor')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'uses defaults when optional params omitted' do
|
|
22
|
+
result = client.apply_stressor(description: 'minimal')
|
|
23
|
+
expect(result).to have_key(:integrity)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'reduces integrity for high-intensity stressor' do
|
|
27
|
+
client.apply_stressor(description: 'overload', intensity: 0.9, duration: 7200)
|
|
28
|
+
status = client.integrity_status
|
|
29
|
+
expect(status[:integrity]).to be < 1.0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'includes breaking flag' do
|
|
33
|
+
result = client.apply_stressor(description: 'test', intensity: 0.5, duration: 3600)
|
|
34
|
+
expect(result).to have_key(:breaking)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#recover' do
|
|
39
|
+
before { client.apply_stressor(description: 'stress', intensity: 0.9, duration: 7200) }
|
|
40
|
+
|
|
41
|
+
it 'increases integrity after recovery' do
|
|
42
|
+
before = client.integrity_status[:integrity]
|
|
43
|
+
client.recover(amount: 1.0)
|
|
44
|
+
after = client.integrity_status[:integrity]
|
|
45
|
+
expect(after).to be >= before
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns a hash with integrity' do
|
|
49
|
+
result = client.recover(amount: 1.0)
|
|
50
|
+
expect(result).to have_key(:integrity)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'uses default amount of 1.0' do
|
|
54
|
+
expect { client.recover }.not_to raise_error
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#rest' do
|
|
59
|
+
before { client.apply_stressor(description: 'stress', intensity: 0.9, duration: 7200) }
|
|
60
|
+
|
|
61
|
+
it 'increases integrity' do
|
|
62
|
+
before = client.integrity_status[:integrity]
|
|
63
|
+
client.rest(amount: 1.0)
|
|
64
|
+
after = client.integrity_status[:integrity]
|
|
65
|
+
expect(after).to be >= before
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns a hash with integrity' do
|
|
69
|
+
result = client.rest(amount: 1.0)
|
|
70
|
+
expect(result).to have_key(:integrity)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#weathering_report' do
|
|
75
|
+
it 'returns a comprehensive report' do
|
|
76
|
+
report = client.weathering_report
|
|
77
|
+
expect(report).to have_key(:integrity)
|
|
78
|
+
expect(report).to have_key(:integrity_label)
|
|
79
|
+
expect(report).to have_key(:effective_capacity)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'reflects stressor history' do
|
|
83
|
+
client.apply_stressor(description: 'first', intensity: 0.5, duration: 3600)
|
|
84
|
+
report = client.weathering_report
|
|
85
|
+
expect(report[:stressor_count]).to eq(1)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#integrity_status' do
|
|
90
|
+
it 'returns all status keys' do
|
|
91
|
+
status = client.integrity_status
|
|
92
|
+
expect(status).to have_key(:integrity)
|
|
93
|
+
expect(status).to have_key(:integrity_label)
|
|
94
|
+
expect(status).to have_key(:tempering_level)
|
|
95
|
+
expect(status).to have_key(:weathering_label)
|
|
96
|
+
expect(status).to have_key(:effective_capacity)
|
|
97
|
+
expect(status).to have_key(:fragile)
|
|
98
|
+
expect(status).to have_key(:breaking)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'starts at pristine with full integrity' do
|
|
102
|
+
status = client.integrity_status
|
|
103
|
+
expect(status[:integrity_label]).to eq('pristine')
|
|
104
|
+
expect(status[:integrity]).to eq(1.0)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
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/cognitive_weathering'
|
|
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,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cognitive-weathering
|
|
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 long-term cognitive wear from sustained workloads based on allostatic
|
|
27
|
+
load theory
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lex-cognitive-weathering.gemspec
|
|
38
|
+
- lib/legion/extensions/cognitive_weathering.rb
|
|
39
|
+
- lib/legion/extensions/cognitive_weathering/client.rb
|
|
40
|
+
- lib/legion/extensions/cognitive_weathering/helpers/constants.rb
|
|
41
|
+
- lib/legion/extensions/cognitive_weathering/helpers/stressor.rb
|
|
42
|
+
- lib/legion/extensions/cognitive_weathering/helpers/weathering_engine.rb
|
|
43
|
+
- lib/legion/extensions/cognitive_weathering/runners/cognitive_weathering.rb
|
|
44
|
+
- lib/legion/extensions/cognitive_weathering/version.rb
|
|
45
|
+
- spec/legion/extensions/cognitive_weathering/client_spec.rb
|
|
46
|
+
- spec/legion/extensions/cognitive_weathering/helpers/constants_spec.rb
|
|
47
|
+
- spec/legion/extensions/cognitive_weathering/helpers/stressor_spec.rb
|
|
48
|
+
- spec/legion/extensions/cognitive_weathering/helpers/weathering_engine_spec.rb
|
|
49
|
+
- spec/legion/extensions/cognitive_weathering/runners/cognitive_weathering_spec.rb
|
|
50
|
+
- spec/spec_helper.rb
|
|
51
|
+
homepage: https://github.com/LegionIO/lex-cognitive-weathering
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-weathering
|
|
56
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-weathering
|
|
57
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-weathering
|
|
58
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-weathering
|
|
59
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-weathering/issues
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.4'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.6.9
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: LEX Cognitive Weathering
|
|
78
|
+
test_files: []
|