lex-cognitive-flexibility-training 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: 98e8996718c4d5c31b8cdb2661bdd30aa5e96eb600819f40486a2f3c5113fe5c
4
+ data.tar.gz: 6d17b53b34d6f6de2db677c277ba0fdcbef55fe71387ad3351f1a2231c1df46c
5
+ SHA512:
6
+ metadata.gz: 17990736352966d80c2eff0c13513c5b5bbbe3de945529427ea01e94d0c13d907fd14fde319ff42f55a47ab021a62dc66197c07f7bbed7523a71dfb01e1dff12
7
+ data.tar.gz: f5d7c7bf3201b1f8ed7f5060e716640e37dc1c63f8e5fe1ed1d1c207b02b2eaf3547298ec127c3dc702ff4db397b694751f3752b62ed8f90b5d930dc4f805483
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ 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,51 @@
1
+ # lex-cognitive-flexibility-training
2
+
3
+ Structured cognitive flexibility training with switch cost measurement for brain-modeled agentic AI in the LegionIO ecosystem.
4
+
5
+ ## What It Does
6
+
7
+ Measures and reduces the cost of switching between cognitive tasks through deliberate practice. Tasks are registered with a domain and difficulty level. Switch trials between task pairs compute a cost based on domain compatibility, difficulty gap, accumulated practice, and fatigue. Over many trials, improvement rate tracks whether switch costs are declining. Sessions group trials for structured training runs.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ require 'legion/extensions/cognitive_flexibility_training'
13
+
14
+ client = Legion::Extensions::CognitiveFlexibilityTraining::Client.new
15
+
16
+ # Register tasks to train on
17
+ r1 = client.register_task(name: 'categorize_inputs', domain: :logical, difficulty: 0.4)
18
+ r2 = client.register_task(name: 'generate_responses', domain: :creative, difficulty: 0.6)
19
+ task_a = r1[:task][:id]
20
+ task_b = r2[:task][:id]
21
+
22
+ # Start a training session
23
+ client.start_training_session
24
+
25
+ # Run switch trials
26
+ client.perform_switch(from_task_id: task_a, to_task_id: task_b)
27
+ client.perform_switch(from_task_id: task_b, to_task_id: task_a)
28
+
29
+ # End session
30
+ client.end_training_session
31
+ # => { success: true, status: :session_ended, trial_count: 2 }
32
+
33
+ # Check improvement
34
+ client.flexibility_score
35
+ # => { success: true, flexibility_score: 0.72, label: :flexible }
36
+
37
+ client.improvement_rate
38
+ # => { success: true, improvement_rate: 0.0 }
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ bundle install
45
+ bundle exec rspec
46
+ bundle exec rubocop
47
+ ```
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_flexibility_training/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-flexibility-training'
7
+ spec.version = Legion::Extensions::CognitiveFlexibilityTraining::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Flexibility Training'
12
+ spec.description = 'Active improvement of task-switching ability through deliberate practice for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-flexibility-training'
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-flexibility-training'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-flexibility-training'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-flexibility-training'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-flexibility-training/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-flexibility-training.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_flexibility_training/helpers/constants'
4
+ require 'legion/extensions/cognitive_flexibility_training/helpers/training_task'
5
+ require 'legion/extensions/cognitive_flexibility_training/helpers/switch_trial'
6
+ require 'legion/extensions/cognitive_flexibility_training/helpers/flexibility_engine'
7
+ require 'legion/extensions/cognitive_flexibility_training/runners/cognitive_flexibility_training'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveFlexibilityTraining
12
+ class Client
13
+ include Runners::CognitiveFlexibilityTraining
14
+
15
+ def initialize(engine: nil, **)
16
+ @flexibility_engine = engine || Helpers::FlexibilityEngine.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :flexibility_engine
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFlexibilityTraining
6
+ module Helpers
7
+ module Constants
8
+ MAX_TASKS = 100
9
+ MAX_TRIALS = 1000
10
+ MAX_SESSIONS = 50
11
+
12
+ DEFAULT_SWITCH_COST = 0.3
13
+ IMPROVEMENT_RATE = 0.02
14
+ FATIGUE_RATE = 0.01
15
+
16
+ DIFFICULTY_LEVELS = %i[trivial easy moderate hard extreme].freeze
17
+ TASK_DOMAINS = %i[linguistic spatial numerical logical emotional social creative].freeze
18
+
19
+ SWITCH_COST_LABELS = {
20
+ (0.8..) => :severe,
21
+ (0.6...0.8) => :high,
22
+ (0.4...0.6) => :moderate,
23
+ (0.2...0.4) => :low,
24
+ (..0.2) => :minimal
25
+ }.freeze
26
+
27
+ FLEXIBILITY_LABELS = {
28
+ (0.8..) => :highly_flexible,
29
+ (0.6...0.8) => :flexible,
30
+ (0.4...0.6) => :moderate,
31
+ (0.2...0.4) => :rigid,
32
+ (..0.2) => :inflexible
33
+ }.freeze
34
+
35
+ PROGRESS_LABELS = {
36
+ (0.8..) => :mastered,
37
+ (0.6...0.8) => :proficient,
38
+ (0.4...0.6) => :developing,
39
+ (0.2...0.4) => :novice,
40
+ (..0.2) => :beginner
41
+ }.freeze
42
+
43
+ module_function
44
+
45
+ def label_for(value, label_map)
46
+ label_map.each { |range, label| return label if range.cover?(value) }
47
+ nil
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFlexibilityTraining
6
+ module Helpers
7
+ class FlexibilityEngine
8
+ attr_reader :tasks, :trials, :sessions
9
+
10
+ def initialize
11
+ @tasks = {}
12
+ @trials = []
13
+ @sessions = []
14
+ @current_session = nil
15
+ end
16
+
17
+ def register_task(name:, domain:, difficulty: 0.5)
18
+ return { error: :max_tasks_reached } if @tasks.size >= Constants::MAX_TASKS
19
+ return { error: :invalid_domain } unless Constants::TASK_DOMAINS.include?(domain)
20
+
21
+ task = TrainingTask.new(name: name, domain: domain, difficulty: difficulty)
22
+ @tasks[task.id] = task
23
+ task
24
+ end
25
+
26
+ def perform_switch(from_task_id:, to_task_id:)
27
+ return { error: :max_trials_reached } if @trials.size >= Constants::MAX_TRIALS
28
+
29
+ from_task = @tasks[from_task_id]
30
+ to_task = @tasks[to_task_id]
31
+
32
+ return { error: :task_not_found, missing: :from } unless from_task
33
+ return { error: :task_not_found, missing: :to } unless to_task
34
+
35
+ cost = compute_switch_cost(from_task, to_task)
36
+ accuracy = compute_accuracy(to_task, cost)
37
+
38
+ trial = SwitchTrial.new(
39
+ from_task_id: from_task_id,
40
+ to_task_id: to_task_id,
41
+ switch_cost: cost,
42
+ accuracy: accuracy
43
+ )
44
+
45
+ @trials << trial
46
+ @current_session << trial if @current_session
47
+
48
+ from_task.practice!
49
+ to_task.practice!
50
+
51
+ trial
52
+ end
53
+
54
+ def start_session
55
+ @current_session = []
56
+ self
57
+ end
58
+
59
+ def end_session
60
+ return nil unless @current_session
61
+
62
+ session = @current_session.dup
63
+ @sessions << session
64
+ @sessions.shift while @sessions.size > Constants::MAX_SESSIONS
65
+ @current_session = nil
66
+ session
67
+ end
68
+
69
+ def average_switch_cost(window: 50)
70
+ recent = @trials.last(window)
71
+ return 0.0 if recent.empty?
72
+
73
+ (recent.sum(&:switch_cost) / recent.size).round(10)
74
+ end
75
+
76
+ def switch_cost_between(from_id:, to_id:)
77
+ pair_trials = @trials.select { |t| t.from_task_id == from_id && t.to_task_id == to_id }
78
+ return 0.0 if pair_trials.empty?
79
+
80
+ (pair_trials.sum(&:switch_cost) / pair_trials.size).round(10)
81
+ end
82
+
83
+ def improvement_rate
84
+ return 0.0 if @trials.size < 4
85
+
86
+ half = @trials.size / 2
87
+ early = @trials.first(half)
88
+ recent = @trials.last(half)
89
+
90
+ early_avg = early.sum(&:switch_cost) / early.size.to_f
91
+ recent_avg = recent.sum(&:switch_cost) / recent.size.to_f
92
+
93
+ ((early_avg - recent_avg) / early_avg.clamp(0.001, 1.0)).round(10)
94
+ end
95
+
96
+ def flexibility_score
97
+ (1.0 - average_switch_cost).round(10)
98
+ end
99
+
100
+ def hardest_switches(limit: 5)
101
+ pair_averages.sort_by { |_, cost| -cost }.first(limit).map do |pair, cost|
102
+ { from_task_id: pair[0], to_task_id: pair[1], average_switch_cost: cost.round(10) }
103
+ end
104
+ end
105
+
106
+ def easiest_switches(limit: 5)
107
+ pair_averages.sort_by { |_, cost| cost }.first(limit).map do |pair, cost|
108
+ { from_task_id: pair[0], to_task_id: pair[1], average_switch_cost: cost.round(10) }
109
+ end
110
+ end
111
+
112
+ def training_report
113
+ {
114
+ task_count: @tasks.size,
115
+ trial_count: @trials.size,
116
+ session_count: @sessions.size,
117
+ average_switch_cost: average_switch_cost,
118
+ flexibility_score: flexibility_score,
119
+ improvement_rate: improvement_rate,
120
+ flexibility_label: Constants.label_for(flexibility_score, Constants::FLEXIBILITY_LABELS),
121
+ progress_label: Constants.label_for(1.0 - average_switch_cost, Constants::PROGRESS_LABELS),
122
+ costly_trial_ratio: costly_ratio,
123
+ successful_trial_ratio: success_ratio
124
+ }
125
+ end
126
+
127
+ def to_h
128
+ {
129
+ tasks: @tasks.transform_values(&:to_h),
130
+ trials: @trials.map(&:to_h),
131
+ sessions: @sessions.map { |s| s.map(&:to_h) },
132
+ report: training_report
133
+ }
134
+ end
135
+
136
+ private
137
+
138
+ def compute_switch_cost(from_task, to_task)
139
+ domain_penalty = from_task.domain == to_task.domain ? 0.0 : 0.2
140
+ difficulty_gap = (to_task.difficulty - from_task.difficulty).abs * 0.3
141
+ practice_discount = [from_task.practice_count * 0.005, 0.15].min
142
+ fatigue_penalty = [@trials.size * Constants::FATIGUE_RATE * 0.01, 0.1].min
143
+
144
+ raw = Constants::DEFAULT_SWITCH_COST + domain_penalty + difficulty_gap - practice_discount + fatigue_penalty
145
+ raw.clamp(0.0, 1.0).round(10)
146
+ end
147
+
148
+ def compute_accuracy(to_task, switch_cost)
149
+ base = to_task.baseline_performance
150
+ penalty = switch_cost * 0.4
151
+ (base - penalty).clamp(0.0, 1.0).round(10)
152
+ end
153
+
154
+ def pair_averages
155
+ grouped = @trials.group_by { |t| [t.from_task_id, t.to_task_id] }
156
+ grouped.transform_values { |ts| ts.sum(&:switch_cost) / ts.size.to_f }
157
+ end
158
+
159
+ def costly_ratio
160
+ return 0.0 if @trials.empty?
161
+
162
+ (@trials.count(&:costly?).to_f / @trials.size).round(10)
163
+ end
164
+
165
+ def success_ratio
166
+ return 0.0 if @trials.empty?
167
+
168
+ (@trials.count(&:successful?).to_f / @trials.size).round(10)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveFlexibilityTraining
8
+ module Helpers
9
+ class SwitchTrial
10
+ attr_reader :id, :from_task_id, :to_task_id, :switch_cost, :accuracy, :created_at
11
+
12
+ def initialize(from_task_id:, to_task_id:, switch_cost:, accuracy:)
13
+ @id = SecureRandom.uuid
14
+ @from_task_id = from_task_id
15
+ @to_task_id = to_task_id
16
+ @switch_cost = switch_cost.clamp(0.0, 1.0).round(10)
17
+ @accuracy = accuracy.clamp(0.0, 1.0).round(10)
18
+ @created_at = Time.now.utc
19
+ end
20
+
21
+ def costly?
22
+ @switch_cost > 0.5
23
+ end
24
+
25
+ def successful?
26
+ @accuracy > 0.7
27
+ end
28
+
29
+ def switch_cost_label
30
+ Constants.label_for(@switch_cost, Constants::SWITCH_COST_LABELS)
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ id: @id,
36
+ from_task_id: @from_task_id,
37
+ to_task_id: @to_task_id,
38
+ switch_cost: @switch_cost,
39
+ accuracy: @accuracy,
40
+ costly: costly?,
41
+ successful: successful?,
42
+ switch_cost_label: switch_cost_label,
43
+ created_at: @created_at
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveFlexibilityTraining
8
+ module Helpers
9
+ class TrainingTask
10
+ attr_reader :id, :name, :domain, :difficulty, :baseline_performance, :practice_count, :created_at
11
+
12
+ def initialize(name:, domain:, difficulty:)
13
+ @id = SecureRandom.uuid
14
+ @name = name
15
+ @domain = domain
16
+ @difficulty = difficulty.clamp(0.0, 1.0).round(10)
17
+ @baseline_performance = (1.0 - (@difficulty * 0.4)).clamp(0.0, 1.0).round(10)
18
+ @practice_count = 0
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def practice!
23
+ @practice_count += 1
24
+ improvement = (Constants::IMPROVEMENT_RATE / (1.0 + (@practice_count * 0.1))).round(10)
25
+ @baseline_performance = [(@baseline_performance + improvement), 1.0].min.round(10)
26
+ self
27
+ end
28
+
29
+ def difficulty_label
30
+ index = (@difficulty * (Constants::DIFFICULTY_LEVELS.size - 1)).round
31
+ Constants::DIFFICULTY_LEVELS[index]
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ id: @id,
37
+ name: @name,
38
+ domain: @domain,
39
+ difficulty: @difficulty,
40
+ baseline_performance: @baseline_performance,
41
+ practice_count: @practice_count,
42
+ difficulty_label: difficulty_label,
43
+ created_at: @created_at
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFlexibilityTraining
6
+ module Runners
7
+ module CognitiveFlexibilityTraining
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def register_task(name:, domain:, difficulty: 0.5, engine: nil, **)
12
+ eng = engine || flexibility_engine
13
+ result = eng.register_task(name: name, domain: domain, difficulty: difficulty)
14
+
15
+ if result.is_a?(Helpers::TrainingTask)
16
+ Legion::Logging.debug "[cft] registered task name=#{name} domain=#{domain} difficulty=#{difficulty}"
17
+ { success: true, task: result.to_h }
18
+ else
19
+ Legion::Logging.warn "[cft] register_task failed: #{result[:error]}"
20
+ { success: false }.merge(result)
21
+ end
22
+ end
23
+
24
+ def perform_switch(from_task_id:, to_task_id:, engine: nil, **)
25
+ eng = engine || flexibility_engine
26
+ result = eng.perform_switch(from_task_id: from_task_id, to_task_id: to_task_id)
27
+
28
+ if result.is_a?(Helpers::SwitchTrial)
29
+ Legion::Logging.debug "[cft] switch trial cost=#{result.switch_cost.round(2)} " \
30
+ "accuracy=#{result.accuracy.round(2)} label=#{result.switch_cost_label}"
31
+ { success: true, trial: result.to_h }
32
+ else
33
+ Legion::Logging.warn "[cft] perform_switch failed: #{result[:error]}"
34
+ { success: false }.merge(result)
35
+ end
36
+ end
37
+
38
+ def start_training_session(engine: nil, **)
39
+ eng = engine || flexibility_engine
40
+ eng.start_session
41
+ Legion::Logging.debug '[cft] training session started'
42
+ { success: true, status: :session_started }
43
+ end
44
+
45
+ def end_training_session(engine: nil, **)
46
+ eng = engine || flexibility_engine
47
+ session = eng.end_session
48
+
49
+ if session
50
+ Legion::Logging.debug "[cft] training session ended trial_count=#{session.size}"
51
+ { success: true, status: :session_ended, trial_count: session.size }
52
+ else
53
+ { success: false, reason: :no_active_session }
54
+ end
55
+ end
56
+
57
+ def average_switch_cost(window: 50, engine: nil, **)
58
+ eng = engine || flexibility_engine
59
+ cost = eng.average_switch_cost(window: window)
60
+ Legion::Logging.debug "[cft] average_switch_cost=#{cost.round(2)} window=#{window}"
61
+ { success: true, average_switch_cost: cost, label: Helpers::Constants.label_for(cost, Helpers::Constants::SWITCH_COST_LABELS) }
62
+ end
63
+
64
+ def switch_cost_between(from_task_id:, to_task_id:, engine: nil, **)
65
+ eng = engine || flexibility_engine
66
+ cost = eng.switch_cost_between(from_id: from_task_id, to_id: to_task_id)
67
+ { success: true, from_task_id: from_task_id, to_task_id: to_task_id,
68
+ average_switch_cost: cost, label: Helpers::Constants.label_for(cost, Helpers::Constants::SWITCH_COST_LABELS) }
69
+ end
70
+
71
+ def flexibility_score(engine: nil, **)
72
+ eng = engine || flexibility_engine
73
+ score = eng.flexibility_score
74
+ Legion::Logging.debug "[cft] flexibility_score=#{score.round(2)}"
75
+ { success: true, flexibility_score: score,
76
+ label: Helpers::Constants.label_for(score, Helpers::Constants::FLEXIBILITY_LABELS) }
77
+ end
78
+
79
+ def improvement_rate(engine: nil, **)
80
+ eng = engine || flexibility_engine
81
+ rate = eng.improvement_rate
82
+ Legion::Logging.debug "[cft] improvement_rate=#{rate.round(2)}"
83
+ { success: true, improvement_rate: rate }
84
+ end
85
+
86
+ def hardest_switches(limit: 5, engine: nil, **)
87
+ eng = engine || flexibility_engine
88
+ results = eng.hardest_switches(limit: limit)
89
+ { success: true, switches: results, count: results.size }
90
+ end
91
+
92
+ def easiest_switches(limit: 5, engine: nil, **)
93
+ eng = engine || flexibility_engine
94
+ results = eng.easiest_switches(limit: limit)
95
+ { success: true, switches: results, count: results.size }
96
+ end
97
+
98
+ def training_report(engine: nil, **)
99
+ eng = engine || flexibility_engine
100
+ report = eng.training_report
101
+ Legion::Logging.debug "[cft] training_report flexibility=#{report[:flexibility_score]&.round(2)}"
102
+ { success: true, report: report }
103
+ end
104
+
105
+ def list_tasks(engine: nil, **)
106
+ eng = engine || flexibility_engine
107
+ { success: true, tasks: eng.tasks.values.map(&:to_h), count: eng.tasks.size }
108
+ end
109
+
110
+ def get_task(task_id:, engine: nil, **)
111
+ eng = engine || flexibility_engine
112
+ task = eng.tasks[task_id]
113
+ task ? { success: true, task: task.to_h } : { success: false, reason: :not_found }
114
+ end
115
+
116
+ def list_trials(engine: nil, **)
117
+ eng = engine || flexibility_engine
118
+ { success: true, trials: eng.trials.map(&:to_h), count: eng.trials.size }
119
+ end
120
+
121
+ private
122
+
123
+ def flexibility_engine
124
+ @flexibility_engine ||= Helpers::FlexibilityEngine.new
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFlexibilityTraining
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_flexibility_training/version'
4
+ require 'legion/extensions/cognitive_flexibility_training/helpers/constants'
5
+ require 'legion/extensions/cognitive_flexibility_training/helpers/training_task'
6
+ require 'legion/extensions/cognitive_flexibility_training/helpers/switch_trial'
7
+ require 'legion/extensions/cognitive_flexibility_training/helpers/flexibility_engine'
8
+ require 'legion/extensions/cognitive_flexibility_training/runners/cognitive_flexibility_training'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module CognitiveFlexibilityTraining
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_flexibility_training/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveFlexibilityTraining::Client do
6
+ it 'responds to all runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:register_task)
9
+ expect(client).to respond_to(:perform_switch)
10
+ expect(client).to respond_to(:start_training_session)
11
+ expect(client).to respond_to(:end_training_session)
12
+ expect(client).to respond_to(:average_switch_cost)
13
+ expect(client).to respond_to(:switch_cost_between)
14
+ expect(client).to respond_to(:flexibility_score)
15
+ expect(client).to respond_to(:improvement_rate)
16
+ expect(client).to respond_to(:hardest_switches)
17
+ expect(client).to respond_to(:easiest_switches)
18
+ expect(client).to respond_to(:training_report)
19
+ expect(client).to respond_to(:list_tasks)
20
+ expect(client).to respond_to(:get_task)
21
+ expect(client).to respond_to(:list_trials)
22
+ end
23
+
24
+ it 'accepts an injected engine' do
25
+ engine = Legion::Extensions::CognitiveFlexibilityTraining::Helpers::FlexibilityEngine.new
26
+ client = described_class.new(engine: engine)
27
+ result = client.register_task(name: 'Test', domain: :logical, difficulty: 0.5, engine: engine)
28
+ expect(result[:success]).to be true
29
+ expect(engine.tasks.size).to eq(1)
30
+ end
31
+
32
+ it 'creates its own engine when none injected' do
33
+ client1 = described_class.new
34
+ client2 = described_class.new
35
+ client1.register_task(name: 'T1', domain: :logical, difficulty: 0.2)
36
+ result = client2.list_tasks
37
+ expect(result[:count]).to eq(0)
38
+ end
39
+ end