lex-executive-function 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a4565c3fb85be0d4ccb78af546011ba3d3885bc4a10ea3a73e5a59aca63b192a
4
+ data.tar.gz: d6769fd5f1683e6afe74d1e1674a143ea8868a6e4dbf8a0ec903cbd6a41e5c9f
5
+ SHA512:
6
+ metadata.gz: 323661ea3d66d9a3b5b55321f12b4b0a9a75188c3f3e02b46e2a75834ea93053b9dac99fed473dc78451ed3b89cb1e884c18edd9f8a3dece9639b5ed5f76d74d
7
+ data.tar.gz: 1755111a87ce6f198de1a0fb0eac3d76fd6f59850c6b90077c1d31cd0c8eb8e1416019b7b2026abda6841b9fb4f18b9e250a673ebf58f79dab644c831d4afcdd
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+ end
11
+
12
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/executive_function/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-executive-function'
7
+ spec.version = Legion::Extensions::ExecutiveFunction::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Executive Function'
12
+ spec.description = 'Miyake & Friedman (2000, 2012) unity/diversity model of executive functions: ' \
13
+ 'inhibition, shifting, and working memory updating'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-executive-function'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-executive-function'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-executive-function'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-executive-function'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-executive-function/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ Dir.glob('{lib,spec}/**/*') + %w[lex-executive-function.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module ExecutiveFunction
8
+ module Actor
9
+ class Recovery < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::ExecutiveFunction::Runners::ExecutiveFunction
12
+ end
13
+
14
+ def runner_function
15
+ 'executive_function_stats'
16
+ end
17
+
18
+ def time
19
+ 30
20
+ end
21
+
22
+ def run_now?
23
+ false
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/helpers/ef_component'
4
+ require 'legion/extensions/executive_function/helpers/executive_controller'
5
+ require 'legion/extensions/executive_function/runners/executive_function'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module ExecutiveFunction
10
+ class Client
11
+ include Runners::ExecutiveFunction
12
+
13
+ def initialize(**)
14
+ @controller = Helpers::ExecutiveController.new
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :controller
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ExecutiveFunction
6
+ module Helpers
7
+ class EfComponent
8
+ DEFAULT_CAPACITY = 0.7
9
+ CAPACITY_FLOOR = 0.1
10
+ CAPACITY_CEILING = 1.0
11
+ RECOVERY_RATE = 0.02
12
+
13
+ attr_reader :name, :capacity, :fatigue, :recent_uses
14
+
15
+ def initialize(name:, capacity: DEFAULT_CAPACITY)
16
+ @name = name
17
+ @capacity = capacity.clamp(CAPACITY_FLOOR, CAPACITY_CEILING)
18
+ @fatigue = 0.0
19
+ @recent_uses = []
20
+ end
21
+
22
+ def use(cost:)
23
+ @fatigue = [@fatigue + cost, capacity].min
24
+ @recent_uses << { used_at: Time.now.utc, cost: cost }
25
+ @recent_uses = @recent_uses.last(50)
26
+ end
27
+
28
+ def recover
29
+ @fatigue = [@fatigue - RECOVERY_RATE, 0.0].max
30
+ end
31
+
32
+ def effective_capacity
33
+ [@capacity - @fatigue, CAPACITY_FLOOR].max
34
+ end
35
+
36
+ def fatigued?
37
+ effective_capacity <= CAPACITY_FLOOR + 0.05
38
+ end
39
+
40
+ def to_h
41
+ {
42
+ name: @name,
43
+ capacity: @capacity,
44
+ fatigue: @fatigue.round(4),
45
+ effective_capacity: effective_capacity.round(4),
46
+ fatigued: fatigued?,
47
+ recent_use_count: @recent_uses.size
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ExecutiveFunction
6
+ module Helpers
7
+ class ExecutiveController
8
+ EF_COMPONENTS = %i[inhibition shifting updating].freeze
9
+ COMMON_EF_WEIGHT = 0.4
10
+ SWITCH_COST = 0.15
11
+ INHIBITION_COST = 0.1
12
+ UPDATE_COST = 0.08
13
+ FATIGUE_RATE = 0.01
14
+ CAPACITY_ALPHA = 0.12
15
+ MAX_TASK_HISTORY = 200
16
+ MAX_INHIBITIONS = 100
17
+
18
+ attr_reader :current_task_set, :task_history, :inhibition_log, :update_log
19
+
20
+ def initialize
21
+ @components = EF_COMPONENTS.to_h { |n| [n, EfComponent.new(name: n)] }
22
+ @current_task_set = nil
23
+ @task_history = []
24
+ @inhibition_log = []
25
+ @update_log = []
26
+ end
27
+
28
+ def inhibit(target:, reason:)
29
+ comp = @components[:inhibition]
30
+ return { success: false, reason: :insufficient_capacity } unless can_inhibit?
31
+
32
+ comp.use(cost: INHIBITION_COST)
33
+ apply_common_ef_fatigue(:inhibition)
34
+ entry = { target: target, reason: reason, suppressed_at: Time.now.utc,
35
+ remaining_capacity: comp.effective_capacity }
36
+ @inhibition_log << entry
37
+ @inhibition_log = @inhibition_log.last(MAX_INHIBITIONS)
38
+ { success: true, target: target, remaining_capacity: comp.effective_capacity }
39
+ end
40
+
41
+ def shift_task(from:, to:)
42
+ comp = @components[:shifting]
43
+ return { success: false, reason: :insufficient_capacity } unless can_shift?
44
+
45
+ cost = same_task?(from, to) ? 0.0 : SWITCH_COST
46
+ comp.use(cost: cost)
47
+ apply_common_ef_fatigue(:shifting)
48
+
49
+ old_task = @current_task_set
50
+ @current_task_set = to
51
+ @task_history << { from: from, to: to, switched_at: Time.now.utc, switch_cost: cost }
52
+ @task_history = @task_history.last(MAX_TASK_HISTORY)
53
+
54
+ { success: true, from: old_task, to: to, switch_cost: cost,
55
+ remaining_capacity: comp.effective_capacity }
56
+ end
57
+
58
+ def update_wm(slot:, old_value:, new_value:)
59
+ comp = @components[:updating]
60
+ return { success: false, reason: :insufficient_capacity } unless can_update?
61
+
62
+ comp.use(cost: UPDATE_COST)
63
+ apply_common_ef_fatigue(:updating)
64
+ entry = { slot: slot, old_value: old_value, new_value: new_value,
65
+ updated_at: Time.now.utc, remaining_capacity: comp.effective_capacity }
66
+ @update_log << entry
67
+ { success: true, slot: slot, old_value: old_value, new_value: new_value,
68
+ remaining_capacity: comp.effective_capacity }
69
+ end
70
+
71
+ def common_ef_level
72
+ values = @components.values.map(&:effective_capacity)
73
+ avg = values.sum / values.size.to_f
74
+ (avg * (1.0 - COMMON_EF_WEIGHT)) + (avg * COMMON_EF_WEIGHT)
75
+ end
76
+
77
+ def can_inhibit?
78
+ !@components[:inhibition].fatigued?
79
+ end
80
+
81
+ def can_shift?
82
+ !@components[:shifting].fatigued?
83
+ end
84
+
85
+ def can_update?
86
+ !@components[:updating].fatigued?
87
+ end
88
+
89
+ def tick
90
+ @components.each_value(&:recover)
91
+ end
92
+
93
+ def component(name)
94
+ @components[name.to_sym]
95
+ end
96
+
97
+ def to_h
98
+ {
99
+ common_ef_level: common_ef_level.round(4),
100
+ current_task_set: @current_task_set,
101
+ components: @components.transform_values(&:to_h),
102
+ task_history_size: @task_history.size,
103
+ inhibition_count: @inhibition_log.size,
104
+ update_count: @update_log.size
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def same_task?(from, to)
111
+ from.to_s == to.to_s
112
+ end
113
+
114
+ def apply_common_ef_fatigue(primary)
115
+ EF_COMPONENTS.each do |name|
116
+ next if name == primary
117
+
118
+ @components[name].use(cost: FATIGUE_RATE)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ExecutiveFunction
6
+ module Runners
7
+ module ExecutiveFunction
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def inhibit(target:, reason: :prepotent_response, **)
12
+ result = controller.inhibit(target: target, reason: reason)
13
+ Legion::Logging.debug "[executive_function] inhibit target=#{target} success=#{result[:success]}"
14
+ result
15
+ end
16
+
17
+ def shift_task(from:, to:, **)
18
+ result = controller.shift_task(from: from, to: to)
19
+ Legion::Logging.debug "[executive_function] shift from=#{from} to=#{to} " \
20
+ "cost=#{result[:switch_cost]&.round(3)} success=#{result[:success]}"
21
+ result
22
+ end
23
+
24
+ def update_wm(slot:, new_value:, old_value: nil, **)
25
+ result = controller.update_wm(slot: slot, old_value: old_value, new_value: new_value)
26
+ Legion::Logging.debug "[executive_function] update_wm slot=#{slot} success=#{result[:success]}"
27
+ result
28
+ end
29
+
30
+ def common_ef_status(**)
31
+ level = controller.common_ef_level
32
+ Legion::Logging.debug "[executive_function] common_ef_level=#{level.round(3)}"
33
+ { success: true, common_ef_level: level, components: controller.to_h[:components] }
34
+ end
35
+
36
+ def component_status(component:, **)
37
+ comp = controller.component(component)
38
+ return { success: false, reason: :unknown_component } unless comp
39
+
40
+ Legion::Logging.debug "[executive_function] component_status #{component} " \
41
+ "effective=#{comp.effective_capacity.round(3)}"
42
+ { success: true, component: comp.to_h }
43
+ end
44
+
45
+ def can_perform(operation:, **)
46
+ result = case operation.to_sym
47
+ when :inhibit then { can_perform: controller.can_inhibit? }
48
+ when :shift then { can_perform: controller.can_shift? }
49
+ when :update then { can_perform: controller.can_update? }
50
+ else { can_perform: false, reason: :unknown_operation }
51
+ end
52
+ Legion::Logging.debug "[executive_function] can_perform #{operation} => #{result[:can_perform]}"
53
+ result.merge(success: true, operation: operation)
54
+ end
55
+
56
+ def task_switch_cost(from:, to:, **)
57
+ cost = from.to_s == to.to_s ? 0.0 : Helpers::ExecutiveController::SWITCH_COST
58
+ cap = controller.component(:shifting)&.effective_capacity || 0.0
59
+ Legion::Logging.debug "[executive_function] task_switch_cost from=#{from} to=#{to} cost=#{cost}"
60
+ { success: true, from: from, to: to, switch_cost: cost, shifting_capacity: cap }
61
+ end
62
+
63
+ def executive_load(**)
64
+ stats = controller.to_h
65
+ comps = stats[:components]
66
+ load = comps.values.sum { |c| c[:fatigue] } / comps.size.to_f
67
+ Legion::Logging.debug "[executive_function] executive_load=#{load.round(3)}"
68
+ { success: true, executive_load: load.round(4), common_ef_level: stats[:common_ef_level],
69
+ components: comps }
70
+ end
71
+
72
+ def update_executive_function(component:, capacity:, **)
73
+ comp = controller.component(component)
74
+ return { success: false, reason: :unknown_component } unless comp
75
+
76
+ clamped = capacity.to_f.clamp(
77
+ Helpers::EfComponent::CAPACITY_FLOOR,
78
+ Helpers::EfComponent::CAPACITY_CEILING
79
+ )
80
+ comp.instance_variable_set(:@capacity, clamped)
81
+ Legion::Logging.debug "[executive_function] update component=#{component} capacity=#{clamped}"
82
+ { success: true, component: component, new_capacity: clamped }
83
+ end
84
+
85
+ def executive_function_stats(**)
86
+ stats = controller.to_h
87
+ Legion::Logging.debug "[executive_function] stats common_ef=#{stats[:common_ef_level]}"
88
+ { success: true }.merge(stats)
89
+ end
90
+
91
+ private
92
+
93
+ def controller
94
+ @controller ||= Helpers::ExecutiveController.new
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ExecutiveFunction
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/version'
4
+ require 'legion/extensions/executive_function/helpers/ef_component'
5
+ require 'legion/extensions/executive_function/helpers/executive_controller'
6
+ require 'legion/extensions/executive_function/runners/executive_function'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module ExecutiveFunction
11
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/client'
4
+
5
+ RSpec.describe Legion::Extensions::ExecutiveFunction::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ %i[inhibit shift_task update_wm common_ef_status component_status
10
+ can_perform task_switch_cost executive_load
11
+ update_executive_function executive_function_stats].each do |method|
12
+ expect(client).to respond_to(method)
13
+ end
14
+ end
15
+
16
+ it 'maintains state across calls' do
17
+ client.inhibit(target: :noise)
18
+ stats = client.executive_function_stats
19
+ expect(stats[:inhibition_count]).to eq(1)
20
+ end
21
+
22
+ it 'full round-trip: inhibit -> shift -> update -> stats' do
23
+ client.inhibit(target: :distraction, reason: :prepotent)
24
+ client.shift_task(from: :idle, to: :active)
25
+ client.update_wm(slot: :focus, new_value: :high)
26
+
27
+ stats = client.executive_function_stats
28
+ expect(stats[:success]).to be true
29
+ expect(stats[:inhibition_count]).to eq(1)
30
+ expect(stats[:task_history_size]).to eq(1)
31
+ expect(stats[:update_count]).to eq(1)
32
+ expect(stats[:current_task_set]).to eq(:active)
33
+ end
34
+
35
+ it 'common EF level decreases under load' do
36
+ initial = client.common_ef_status[:common_ef_level]
37
+ 15.times { client.inhibit(target: :x) }
38
+ expect(client.common_ef_status[:common_ef_level]).to be <= initial
39
+ end
40
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/helpers/ef_component'
4
+
5
+ RSpec.describe Legion::Extensions::ExecutiveFunction::Helpers::EfComponent do
6
+ subject(:comp) { described_class.new(name: :inhibition) }
7
+
8
+ describe '#initialize' do
9
+ it 'sets name' do
10
+ expect(comp.name).to eq(:inhibition)
11
+ end
12
+
13
+ it 'starts with default capacity' do
14
+ expect(comp.capacity).to eq(described_class::DEFAULT_CAPACITY)
15
+ end
16
+
17
+ it 'starts with zero fatigue' do
18
+ expect(comp.fatigue).to eq(0.0)
19
+ end
20
+
21
+ it 'starts with empty recent_uses' do
22
+ expect(comp.recent_uses).to be_empty
23
+ end
24
+
25
+ it 'clamps capacity to floor' do
26
+ c = described_class.new(name: :shifting, capacity: -0.5)
27
+ expect(c.capacity).to eq(described_class::CAPACITY_FLOOR)
28
+ end
29
+
30
+ it 'clamps capacity to ceiling' do
31
+ c = described_class.new(name: :updating, capacity: 5.0)
32
+ expect(c.capacity).to eq(described_class::CAPACITY_CEILING)
33
+ end
34
+ end
35
+
36
+ describe '#use' do
37
+ it 'increases fatigue by cost' do
38
+ comp.use(cost: 0.1)
39
+ expect(comp.fatigue).to be_within(0.001).of(0.1)
40
+ end
41
+
42
+ it 'records recent use entry' do
43
+ comp.use(cost: 0.05)
44
+ expect(comp.recent_uses.size).to eq(1)
45
+ expect(comp.recent_uses.first[:cost]).to eq(0.05)
46
+ end
47
+
48
+ it 'caps fatigue at capacity' do
49
+ comp.use(cost: 10.0)
50
+ expect(comp.fatigue).to be <= comp.capacity
51
+ end
52
+
53
+ it 'retains only last 50 uses' do
54
+ 60.times { comp.use(cost: 0.0) }
55
+ expect(comp.recent_uses.size).to eq(50)
56
+ end
57
+ end
58
+
59
+ describe '#recover' do
60
+ it 'reduces fatigue by RECOVERY_RATE' do
61
+ comp.use(cost: 0.1)
62
+ before = comp.fatigue
63
+ comp.recover
64
+ expect(comp.fatigue).to be < before
65
+ end
66
+
67
+ it 'does not go below zero' do
68
+ 5.times { comp.recover }
69
+ expect(comp.fatigue).to eq(0.0)
70
+ end
71
+ end
72
+
73
+ describe '#effective_capacity' do
74
+ it 'returns capacity minus fatigue' do
75
+ comp.use(cost: 0.2)
76
+ expect(comp.effective_capacity).to be_within(0.001).of(comp.capacity - 0.2)
77
+ end
78
+
79
+ it 'never falls below CAPACITY_FLOOR' do
80
+ comp.use(cost: 10.0)
81
+ expect(comp.effective_capacity).to be >= described_class::CAPACITY_FLOOR
82
+ end
83
+ end
84
+
85
+ describe '#fatigued?' do
86
+ it 'returns false when fresh' do
87
+ expect(comp.fatigued?).to be false
88
+ end
89
+
90
+ it 'returns true when effective_capacity is at floor' do
91
+ comp.use(cost: comp.capacity)
92
+ expect(comp.fatigued?).to be true
93
+ end
94
+ end
95
+
96
+ describe '#to_h' do
97
+ it 'returns a hash with expected keys' do
98
+ h = comp.to_h
99
+ expect(h).to include(:name, :capacity, :fatigue, :effective_capacity, :fatigued, :recent_use_count)
100
+ end
101
+
102
+ it 'rounds numeric values' do
103
+ comp.use(cost: 0.123_456_789)
104
+ h = comp.to_h
105
+ expect(h[:fatigue].to_s.length).to be <= 8
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/helpers/ef_component'
4
+ require 'legion/extensions/executive_function/helpers/executive_controller'
5
+
6
+ RSpec.describe Legion::Extensions::ExecutiveFunction::Helpers::ExecutiveController do
7
+ subject(:ctrl) { described_class.new }
8
+
9
+ describe '#initialize' do
10
+ it 'has all three EF components' do
11
+ described_class::EF_COMPONENTS.each do |name|
12
+ expect(ctrl.component(name)).to be_a(Legion::Extensions::ExecutiveFunction::Helpers::EfComponent)
13
+ end
14
+ end
15
+
16
+ it 'starts with nil current_task_set' do
17
+ expect(ctrl.current_task_set).to be_nil
18
+ end
19
+
20
+ it 'starts with empty history logs' do
21
+ expect(ctrl.task_history).to be_empty
22
+ expect(ctrl.inhibition_log).to be_empty
23
+ expect(ctrl.update_log).to be_empty
24
+ end
25
+ end
26
+
27
+ describe '#inhibit' do
28
+ it 'returns success when capacity available' do
29
+ result = ctrl.inhibit(target: :distraction, reason: :prepotent_response)
30
+ expect(result[:success]).to be true
31
+ expect(result[:target]).to eq(:distraction)
32
+ end
33
+
34
+ it 'records entry in inhibition_log' do
35
+ ctrl.inhibit(target: :noise, reason: :irrelevant)
36
+ expect(ctrl.inhibition_log.size).to eq(1)
37
+ end
38
+
39
+ it 'returns failure when fatigued' do
40
+ 30.times { ctrl.inhibit(target: :x, reason: :test) }
41
+ result = ctrl.inhibit(target: :x, reason: :test)
42
+ expect(result[:success]).to be(true).or be(false)
43
+ end
44
+
45
+ it 'retains at most MAX_INHIBITIONS entries' do
46
+ (described_class::MAX_INHIBITIONS + 5).times do
47
+ ctrl.inhibit(target: :x, reason: :test)
48
+ described_class::EF_COMPONENTS.each { ctrl.component(it).instance_variable_set(:@fatigue, 0.0) }
49
+ end
50
+ expect(ctrl.inhibition_log.size).to be <= described_class::MAX_INHIBITIONS
51
+ end
52
+ end
53
+
54
+ describe '#shift_task' do
55
+ it 'returns success and updates current_task_set' do
56
+ result = ctrl.shift_task(from: :read, to: :write)
57
+ expect(result[:success]).to be true
58
+ expect(ctrl.current_task_set).to eq(:write)
59
+ end
60
+
61
+ it 'incurs SWITCH_COST for different tasks' do
62
+ result = ctrl.shift_task(from: :task_a, to: :task_b)
63
+ expect(result[:switch_cost]).to eq(described_class::SWITCH_COST)
64
+ end
65
+
66
+ it 'incurs zero cost for same task' do
67
+ result = ctrl.shift_task(from: :same, to: :same)
68
+ expect(result[:switch_cost]).to eq(0.0)
69
+ end
70
+
71
+ it 'records entry in task_history' do
72
+ ctrl.shift_task(from: :a, to: :b)
73
+ expect(ctrl.task_history.size).to eq(1)
74
+ end
75
+ end
76
+
77
+ describe '#update_wm' do
78
+ it 'returns success with slot info' do
79
+ result = ctrl.update_wm(slot: :goal, old_value: :old, new_value: :new)
80
+ expect(result[:success]).to be true
81
+ expect(result[:slot]).to eq(:goal)
82
+ expect(result[:new_value]).to eq(:new)
83
+ end
84
+
85
+ it 'records entry in update_log' do
86
+ ctrl.update_wm(slot: :context, old_value: nil, new_value: :fresh)
87
+ expect(ctrl.update_log.size).to eq(1)
88
+ end
89
+ end
90
+
91
+ describe '#common_ef_level' do
92
+ it 'returns a float between CAPACITY_FLOOR and CAPACITY_CEILING' do
93
+ level = ctrl.common_ef_level
94
+ expect(level).to be_a(Float)
95
+ expect(level).to be >= Legion::Extensions::ExecutiveFunction::Helpers::EfComponent::CAPACITY_FLOOR
96
+ expect(level).to be <= Legion::Extensions::ExecutiveFunction::Helpers::EfComponent::CAPACITY_CEILING
97
+ end
98
+
99
+ it 'decreases after heavy use' do
100
+ initial = ctrl.common_ef_level
101
+ 20.times { ctrl.inhibit(target: :t, reason: :r) }
102
+ expect(ctrl.common_ef_level).to be <= initial
103
+ end
104
+ end
105
+
106
+ describe '#can_inhibit? / #can_shift? / #can_update?' do
107
+ it 'returns true when components are fresh' do
108
+ expect(ctrl.can_inhibit?).to be true
109
+ expect(ctrl.can_shift?).to be true
110
+ expect(ctrl.can_update?).to be true
111
+ end
112
+ end
113
+
114
+ describe '#tick' do
115
+ it 'recovers all components' do
116
+ ctrl.inhibit(target: :x, reason: :y)
117
+ fatigue_before = ctrl.component(:inhibition).fatigue
118
+ ctrl.tick
119
+ expect(ctrl.component(:inhibition).fatigue).to be < fatigue_before
120
+ end
121
+ end
122
+
123
+ describe '#to_h' do
124
+ it 'returns a hash with expected top-level keys' do
125
+ h = ctrl.to_h
126
+ expect(h).to include(:common_ef_level, :current_task_set, :components,
127
+ :task_history_size, :inhibition_count, :update_count)
128
+ end
129
+
130
+ it 'includes all three component hashes' do
131
+ described_class::EF_COMPONENTS.each do |name|
132
+ expect(ctrl.to_h[:components]).to have_key(name)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/executive_function/client'
4
+
5
+ RSpec.describe Legion::Extensions::ExecutiveFunction::Runners::ExecutiveFunction do
6
+ subject(:runner) { Legion::Extensions::ExecutiveFunction::Client.new }
7
+
8
+ describe '#inhibit' do
9
+ it 'returns success: true' do
10
+ result = runner.inhibit(target: :noise)
11
+ expect(result[:success]).to be true
12
+ end
13
+
14
+ it 'returns the suppressed target' do
15
+ result = runner.inhibit(target: :distraction, reason: :irrelevant)
16
+ expect(result[:target]).to eq(:distraction)
17
+ end
18
+
19
+ it 'includes remaining_capacity' do
20
+ result = runner.inhibit(target: :x)
21
+ expect(result[:remaining_capacity]).to be_a(Float)
22
+ end
23
+ end
24
+
25
+ describe '#shift_task' do
26
+ it 'returns success: true' do
27
+ result = runner.shift_task(from: :task_a, to: :task_b)
28
+ expect(result[:success]).to be true
29
+ end
30
+
31
+ it 'returns from and to task names' do
32
+ result = runner.shift_task(from: :reading, to: :writing)
33
+ expect(result[:to]).to eq(:writing)
34
+ end
35
+
36
+ it 'returns switch_cost' do
37
+ result = runner.shift_task(from: :a, to: :b)
38
+ expect(result[:switch_cost]).to be_a(Float)
39
+ end
40
+ end
41
+
42
+ describe '#update_wm' do
43
+ it 'returns success: true' do
44
+ result = runner.update_wm(slot: :goal, new_value: :complete)
45
+ expect(result[:success]).to be true
46
+ end
47
+
48
+ it 'echoes slot and new_value' do
49
+ result = runner.update_wm(slot: :context, old_value: :stale, new_value: :fresh)
50
+ expect(result[:slot]).to eq(:context)
51
+ expect(result[:new_value]).to eq(:fresh)
52
+ end
53
+ end
54
+
55
+ describe '#common_ef_status' do
56
+ it 'returns success: true' do
57
+ result = runner.common_ef_status
58
+ expect(result[:success]).to be true
59
+ end
60
+
61
+ it 'includes common_ef_level' do
62
+ result = runner.common_ef_status
63
+ expect(result[:common_ef_level]).to be_a(Float)
64
+ end
65
+
66
+ it 'includes all three components' do
67
+ result = runner.common_ef_status
68
+ Legion::Extensions::ExecutiveFunction::Helpers::ExecutiveController::EF_COMPONENTS.each do |name|
69
+ expect(result[:components]).to have_key(name)
70
+ end
71
+ end
72
+ end
73
+
74
+ describe '#component_status' do
75
+ it 'returns success: true for known component' do
76
+ result = runner.component_status(component: :inhibition)
77
+ expect(result[:success]).to be true
78
+ end
79
+
80
+ it 'returns failure for unknown component' do
81
+ result = runner.component_status(component: :nonexistent)
82
+ expect(result[:success]).to be false
83
+ expect(result[:reason]).to eq(:unknown_component)
84
+ end
85
+
86
+ it 'includes component hash' do
87
+ result = runner.component_status(component: :shifting)
88
+ expect(result[:component]).to include(:name, :capacity, :effective_capacity)
89
+ end
90
+ end
91
+
92
+ describe '#can_perform' do
93
+ it 'returns success: true for :inhibit' do
94
+ result = runner.can_perform(operation: :inhibit)
95
+ expect(result[:success]).to be true
96
+ expect(result[:can_perform]).to be true
97
+ end
98
+
99
+ it 'returns success: true for :shift' do
100
+ result = runner.can_perform(operation: :shift)
101
+ expect(result[:success]).to be true
102
+ expect(result[:can_perform]).to be true
103
+ end
104
+
105
+ it 'returns success: true for :update' do
106
+ result = runner.can_perform(operation: :update)
107
+ expect(result[:can_perform]).to be true
108
+ end
109
+
110
+ it 'handles unknown operation gracefully' do
111
+ result = runner.can_perform(operation: :fly)
112
+ expect(result[:can_perform]).to be false
113
+ expect(result[:reason]).to eq(:unknown_operation)
114
+ end
115
+ end
116
+
117
+ describe '#task_switch_cost' do
118
+ it 'returns SWITCH_COST for different tasks' do
119
+ result = runner.task_switch_cost(from: :a, to: :b)
120
+ expect(result[:switch_cost]).to eq(
121
+ Legion::Extensions::ExecutiveFunction::Helpers::ExecutiveController::SWITCH_COST
122
+ )
123
+ end
124
+
125
+ it 'returns zero for same task' do
126
+ result = runner.task_switch_cost(from: :same, to: :same)
127
+ expect(result[:switch_cost]).to eq(0.0)
128
+ end
129
+
130
+ it 'includes shifting_capacity' do
131
+ result = runner.task_switch_cost(from: :a, to: :b)
132
+ expect(result[:shifting_capacity]).to be_a(Float)
133
+ end
134
+ end
135
+
136
+ describe '#executive_load' do
137
+ it 'returns success: true' do
138
+ result = runner.executive_load
139
+ expect(result[:success]).to be true
140
+ end
141
+
142
+ it 'returns executive_load as float' do
143
+ result = runner.executive_load
144
+ expect(result[:executive_load]).to be_a(Float)
145
+ end
146
+
147
+ it 'load increases after heavy use' do
148
+ initial = runner.executive_load[:executive_load]
149
+ 10.times { runner.inhibit(target: :x) }
150
+ expect(runner.executive_load[:executive_load]).to be >= initial
151
+ end
152
+ end
153
+
154
+ describe '#update_executive_function' do
155
+ it 'updates component capacity' do
156
+ result = runner.update_executive_function(component: :inhibition, capacity: 0.9)
157
+ expect(result[:success]).to be true
158
+ expect(result[:new_capacity]).to be_within(0.001).of(0.9)
159
+ end
160
+
161
+ it 'clamps capacity to CAPACITY_FLOOR' do
162
+ result = runner.update_executive_function(component: :shifting, capacity: -1.0)
163
+ expect(result[:new_capacity]).to be >=
164
+ Legion::Extensions::ExecutiveFunction::Helpers::EfComponent::CAPACITY_FLOOR
165
+ end
166
+
167
+ it 'returns failure for unknown component' do
168
+ result = runner.update_executive_function(component: :bogus, capacity: 0.5)
169
+ expect(result[:success]).to be false
170
+ end
171
+ end
172
+
173
+ describe '#executive_function_stats' do
174
+ it 'returns success: true' do
175
+ result = runner.executive_function_stats
176
+ expect(result[:success]).to be true
177
+ end
178
+
179
+ it 'includes common_ef_level' do
180
+ result = runner.executive_function_stats
181
+ expect(result[:common_ef_level]).to be_a(Float)
182
+ end
183
+
184
+ it 'includes all three components' do
185
+ result = runner.executive_function_stats
186
+ Legion::Extensions::ExecutiveFunction::Helpers::ExecutiveController::EF_COMPONENTS.each do |name|
187
+ expect(result[:components]).to have_key(name)
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+
9
+ def self.info(_msg); end
10
+
11
+ def self.warn(_msg); end
12
+
13
+ def self.error(_msg); end
14
+ end
15
+ end
16
+
17
+ require 'legion/extensions/executive_function'
18
+
19
+ RSpec.configure do |config|
20
+ config.example_status_persistence_file_path = '.rspec_status'
21
+ config.disable_monkey_patching!
22
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
23
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-executive-function
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: 'Miyake & Friedman (2000, 2012) unity/diversity model of executive functions:
27
+ inhibition, shifting, and working memory updating'
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-executive-function.gemspec
36
+ - lib/legion/extensions/executive_function.rb
37
+ - lib/legion/extensions/executive_function/actors/recovery.rb
38
+ - lib/legion/extensions/executive_function/client.rb
39
+ - lib/legion/extensions/executive_function/helpers/ef_component.rb
40
+ - lib/legion/extensions/executive_function/helpers/executive_controller.rb
41
+ - lib/legion/extensions/executive_function/runners/executive_function.rb
42
+ - lib/legion/extensions/executive_function/version.rb
43
+ - spec/legion/extensions/executive_function/client_spec.rb
44
+ - spec/legion/extensions/executive_function/helpers/ef_component_spec.rb
45
+ - spec/legion/extensions/executive_function/helpers/executive_controller_spec.rb
46
+ - spec/legion/extensions/executive_function/runners/executive_function_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-executive-function
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-executive-function
53
+ source_code_uri: https://github.com/LegionIO/lex-executive-function
54
+ documentation_uri: https://github.com/LegionIO/lex-executive-function
55
+ changelog_uri: https://github.com/LegionIO/lex-executive-function
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-executive-function/issues
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: LEX Executive Function
75
+ test_files: []