lex-cognitive-apprenticeship 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: 9e2a7e552bc6b9ab40bf5ba2a02975c5a3306774558ab7e80e517167d058a5f8
4
+ data.tar.gz: 5f3bb14640b7ac611036ef4b0578abf03eb28d6521168ce5a44ac8523399f5db
5
+ SHA512:
6
+ metadata.gz: 8fc0cb1baaddd2da7583f48156ab5571bd04391a52802de11ab003866d731ad00e96ad8f5ecb05c48738846a01639acc268881fc294b2e5c44ed9c8dbfc03a9f
7
+ data.tar.gz: 4d2162f1c7269d7dcccb302f6fad5a157ab7c3d0ee58144703487eb1f7f26f2446893593ebc0674847a500a033272871ea1a4a7b08852475debba295c62b745c
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'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_apprenticeship/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-apprenticeship'
7
+ spec.version = Legion::Extensions::CognitiveApprenticeship::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Apprenticeship'
12
+ spec.description = "Collins' Cognitive Apprenticeship model for brain-modeled agentic AI: " \
13
+ 'guided learning through modeling, coaching, scaffolding, articulation, reflection, and exploration'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
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-cognitive-apprenticeship'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship/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-cognitive-apprenticeship.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model'
4
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship'
5
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine'
6
+ require 'legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module CognitiveApprenticeship
11
+ class Client
12
+ include Runners::CognitiveApprenticeship
13
+
14
+ def initialize(**)
15
+ @engine = Helpers::ApprenticeshipEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveApprenticeship
6
+ module Helpers
7
+ class Apprenticeship
8
+ include ApprenticeshipModel
9
+
10
+ attr_reader :id, :skill_name, :domain, :mentor_id, :apprentice_id,
11
+ :mastery, :session_count, :created_at, :last_session_at
12
+
13
+ def initialize(skill_name:, domain:, mentor_id:, apprentice_id:)
14
+ data = ApprenticeshipModel.new_apprenticeship(
15
+ skill_name: skill_name,
16
+ domain: domain,
17
+ mentor_id: mentor_id,
18
+ apprentice_id: apprentice_id
19
+ )
20
+ @id = data[:id]
21
+ @skill_name = data[:skill_name]
22
+ @domain = data[:domain]
23
+ @mentor_id = data[:mentor_id]
24
+ @apprentice_id = data[:apprentice_id]
25
+ @mastery = data[:mastery]
26
+ @session_count = data[:session_count]
27
+ @created_at = data[:created_at]
28
+ @last_session_at = data[:last_session_at]
29
+ end
30
+
31
+ def current_phase
32
+ ApprenticeshipModel.phase_for(@mastery)
33
+ end
34
+
35
+ def mastery_label
36
+ ApprenticeshipModel.mastery_label_for(@mastery)
37
+ end
38
+
39
+ def graduated?
40
+ @mastery >= ApprenticeshipModel::MASTERY_THRESHOLD
41
+ end
42
+
43
+ def recommended_method
44
+ ApprenticeshipModel.phase_for(@mastery)
45
+ end
46
+
47
+ def learn!(method:, success:)
48
+ multiplier = case method
49
+ when :exploration then ApprenticeshipModel::EXPLORATION_MULTIPLIER
50
+ when :coaching then ApprenticeshipModel::COACHING_MULTIPLIER
51
+ else 1.0
52
+ end
53
+
54
+ gain = success ? ApprenticeshipModel::LEARNING_GAIN * multiplier : 0.0
55
+ @mastery = ApprenticeshipModel.clamp_mastery(@mastery + gain)
56
+ @session_count += 1
57
+ @last_session_at = Time.now.utc
58
+ self
59
+ end
60
+
61
+ def decay!
62
+ @mastery = ApprenticeshipModel.clamp_mastery(@mastery - ApprenticeshipModel::DECAY_RATE)
63
+ self
64
+ end
65
+
66
+ def to_h
67
+ {
68
+ id: @id,
69
+ skill_name: @skill_name,
70
+ domain: @domain,
71
+ mentor_id: @mentor_id,
72
+ apprentice_id: @apprentice_id,
73
+ mastery: @mastery,
74
+ current_phase: current_phase,
75
+ mastery_label: mastery_label,
76
+ graduated: graduated?,
77
+ session_count: @session_count,
78
+ created_at: @created_at,
79
+ last_session_at: @last_session_at
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveApprenticeship
6
+ module Helpers
7
+ class ApprenticeshipEngine
8
+ def initialize
9
+ @apprenticeships = {}
10
+ @sessions = []
11
+ end
12
+
13
+ def create_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:)
14
+ return nil if @apprenticeships.size >= ApprenticeshipModel::MAX_APPRENTICESHIPS
15
+
16
+ appr = Apprenticeship.new(
17
+ skill_name: skill_name,
18
+ domain: domain,
19
+ mentor_id: mentor_id,
20
+ apprentice_id: apprentice_id
21
+ )
22
+ @apprenticeships[appr.id] = appr
23
+ appr
24
+ end
25
+
26
+ def conduct_session(apprenticeship_id:, method:, success:)
27
+ appr = @apprenticeships[apprenticeship_id]
28
+ return nil unless appr
29
+
30
+ appr.learn!(method: method, success: success)
31
+ record_session(apprenticeship_id: apprenticeship_id, method: method, success: success)
32
+ appr
33
+ end
34
+
35
+ def recommend_method(apprenticeship_id:)
36
+ appr = @apprenticeships[apprenticeship_id]
37
+ return nil unless appr
38
+
39
+ appr.recommended_method
40
+ end
41
+
42
+ def graduated_apprenticeships
43
+ @apprenticeships.values.select(&:graduated?)
44
+ end
45
+
46
+ def active_apprenticeships
47
+ @apprenticeships.values.reject(&:graduated?)
48
+ end
49
+
50
+ def by_mentor(mentor_id:)
51
+ @apprenticeships.values.select { |a| a.mentor_id == mentor_id }
52
+ end
53
+
54
+ def by_apprentice(apprentice_id:)
55
+ @apprenticeships.values.select { |a| a.apprentice_id == apprentice_id }
56
+ end
57
+
58
+ def by_domain(domain:)
59
+ @apprenticeships.values.select { |a| a.domain == domain }
60
+ end
61
+
62
+ def decay_all
63
+ @apprenticeships.each_value(&:decay!)
64
+ @apprenticeships.size
65
+ end
66
+
67
+ def get(apprenticeship_id)
68
+ @apprenticeships[apprenticeship_id]
69
+ end
70
+
71
+ def count
72
+ @apprenticeships.size
73
+ end
74
+
75
+ def sessions
76
+ @sessions.dup
77
+ end
78
+
79
+ def to_h
80
+ {
81
+ total: @apprenticeships.size,
82
+ active: active_apprenticeships.size,
83
+ graduated: graduated_apprenticeships.size,
84
+ sessions: @sessions.size
85
+ }
86
+ end
87
+
88
+ private
89
+
90
+ def record_session(apprenticeship_id:, method:, success:)
91
+ return if @sessions.size >= ApprenticeshipModel::MAX_SESSIONS
92
+
93
+ @sessions << {
94
+ apprenticeship_id: apprenticeship_id,
95
+ method: method,
96
+ success: success,
97
+ recorded_at: Time.now.utc
98
+ }
99
+ @sessions.shift while @sessions.size > ApprenticeshipModel::MAX_HISTORY
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveApprenticeship
8
+ module Helpers
9
+ module ApprenticeshipModel
10
+ # Capacity limits
11
+ MAX_APPRENTICESHIPS = 100
12
+ MAX_SESSIONS = 500
13
+ MAX_HISTORY = 300
14
+
15
+ # Mastery defaults and bounds
16
+ DEFAULT_MASTERY = 0.1
17
+ MASTERY_FLOOR = 0.0
18
+ MASTERY_CEILING = 1.0
19
+ MASTERY_THRESHOLD = 0.85
20
+ PHASE_THRESHOLD = 0.5
21
+
22
+ # Learning dynamics
23
+ LEARNING_GAIN = 0.08
24
+ COACHING_MULTIPLIER = 1.5
25
+ EXPLORATION_MULTIPLIER = 2.0
26
+ DECAY_RATE = 0.01
27
+
28
+ # Collins' six instructional methods
29
+ METHODS = %i[modeling coaching scaffolding articulation reflection exploration].freeze
30
+
31
+ # Phase labels derived from mastery range
32
+ PHASE_LABELS = {
33
+ (0.0...0.2) => :modeling,
34
+ (0.2...0.4) => :coaching,
35
+ (0.4...0.6) => :scaffolding,
36
+ (0.6...0.75) => :articulation,
37
+ (0.75...0.85) => :reflection,
38
+ (0.85..1.0) => :exploration
39
+ }.freeze
40
+
41
+ # Mastery level labels
42
+ MASTERY_LABELS = {
43
+ (0.8..) => :expert,
44
+ (0.6...0.8) => :proficient,
45
+ (0.4...0.6) => :intermediate,
46
+ (0.2...0.4) => :apprentice,
47
+ (..0.2) => :novice
48
+ }.freeze
49
+
50
+ module_function
51
+
52
+ def phase_for(mastery)
53
+ PHASE_LABELS.each do |range, phase|
54
+ return phase if range.cover?(mastery)
55
+ end
56
+ :exploration
57
+ end
58
+
59
+ def mastery_label_for(mastery)
60
+ MASTERY_LABELS.each do |range, label|
61
+ return label if range.cover?(mastery)
62
+ end
63
+ :expert
64
+ end
65
+
66
+ def clamp_mastery(value)
67
+ value.clamp(MASTERY_FLOOR, MASTERY_CEILING)
68
+ end
69
+
70
+ def new_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:)
71
+ now = Time.now.utc
72
+ {
73
+ id: SecureRandom.uuid,
74
+ skill_name: skill_name,
75
+ domain: domain,
76
+ mentor_id: mentor_id,
77
+ apprentice_id: apprentice_id,
78
+ mastery: DEFAULT_MASTERY,
79
+ current_phase: phase_for(DEFAULT_MASTERY),
80
+ session_count: 0,
81
+ created_at: now,
82
+ last_session_at: nil
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveApprenticeship
6
+ module Runners
7
+ module CognitiveApprenticeship
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:, **)
12
+ return { success: false, reason: :param_too_short } if [skill_name, domain, mentor_id, apprentice_id].any? { |p| p.to_s.length < 3 }
13
+
14
+ appr = engine.create_apprenticeship(
15
+ skill_name: skill_name,
16
+ domain: domain,
17
+ mentor_id: mentor_id,
18
+ apprentice_id: apprentice_id
19
+ )
20
+
21
+ if appr
22
+ Legion::Logging.info "[cognitive_apprenticeship] created id=#{appr.id} skill=#{skill_name} domain=#{domain}"
23
+ { success: true, apprenticeship: appr.to_h }
24
+ else
25
+ Legion::Logging.warn '[cognitive_apprenticeship] create failed: capacity reached'
26
+ { success: false, reason: :capacity_reached }
27
+ end
28
+ end
29
+
30
+ def conduct_apprenticeship_session(apprenticeship_id:, method:, success:, **)
31
+ return { success: false, reason: :invalid_method } unless Helpers::ApprenticeshipModel::METHODS.include?(method.to_sym)
32
+
33
+ appr = engine.conduct_session(
34
+ apprenticeship_id: apprenticeship_id,
35
+ method: method.to_sym,
36
+ success: success
37
+ )
38
+
39
+ if appr
40
+ Legion::Logging.debug "[cognitive_apprenticeship] session id=#{apprenticeship_id} method=#{method} " \
41
+ "success=#{success} mastery=#{appr.mastery.round(3)}"
42
+ { success: true, apprenticeship: appr.to_h }
43
+ else
44
+ { success: false, reason: :not_found }
45
+ end
46
+ end
47
+
48
+ def recommend_apprenticeship_method(apprenticeship_id:, **)
49
+ method = engine.recommend_method(apprenticeship_id: apprenticeship_id)
50
+
51
+ if method
52
+ Legion::Logging.debug "[cognitive_apprenticeship] recommend id=#{apprenticeship_id} method=#{method}"
53
+ { success: true, apprenticeship_id: apprenticeship_id, recommended_method: method }
54
+ else
55
+ { success: false, reason: :not_found }
56
+ end
57
+ end
58
+
59
+ def graduated_apprenticeships(**)
60
+ list = engine.graduated_apprenticeships
61
+ Legion::Logging.debug "[cognitive_apprenticeship] graduated count=#{list.size}"
62
+ { success: true, apprenticeships: list.map(&:to_h), count: list.size }
63
+ end
64
+
65
+ def active_apprenticeships(**)
66
+ list = engine.active_apprenticeships
67
+ Legion::Logging.debug "[cognitive_apprenticeship] active count=#{list.size}"
68
+ { success: true, apprenticeships: list.map(&:to_h), count: list.size }
69
+ end
70
+
71
+ def mentor_apprenticeships(mentor_id:, **)
72
+ list = engine.by_mentor(mentor_id: mentor_id)
73
+ Legion::Logging.debug "[cognitive_apprenticeship] mentor=#{mentor_id} count=#{list.size}"
74
+ { success: true, mentor_id: mentor_id, apprenticeships: list.map(&:to_h), count: list.size }
75
+ end
76
+
77
+ def apprentice_apprenticeships(apprentice_id:, **)
78
+ list = engine.by_apprentice(apprentice_id: apprentice_id)
79
+ Legion::Logging.debug "[cognitive_apprenticeship] apprentice=#{apprentice_id} count=#{list.size}"
80
+ { success: true, apprentice_id: apprentice_id, apprenticeships: list.map(&:to_h), count: list.size }
81
+ end
82
+
83
+ def domain_apprenticeships(domain:, **)
84
+ list = engine.by_domain(domain: domain)
85
+ Legion::Logging.debug "[cognitive_apprenticeship] domain=#{domain} count=#{list.size}"
86
+ { success: true, domain: domain, apprenticeships: list.map(&:to_h), count: list.size }
87
+ end
88
+
89
+ def update_cognitive_apprenticeship(apprenticeship_id:, method:, success:, **)
90
+ conduct_apprenticeship_session(apprenticeship_id: apprenticeship_id, method: method, success: success)
91
+ end
92
+
93
+ def cognitive_apprenticeship_stats(**)
94
+ stats = engine.to_h
95
+ Legion::Logging.debug "[cognitive_apprenticeship] stats=#{stats.inspect}"
96
+ { success: true }.merge(stats)
97
+ end
98
+
99
+ private
100
+
101
+ def engine
102
+ @engine ||= Helpers::ApprenticeshipEngine.new
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveApprenticeship
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_apprenticeship/version'
4
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model'
5
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship'
6
+ require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine'
7
+ require 'legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveApprenticeship
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_apprenticeship/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:create_apprenticeship)
10
+ expect(client).to respond_to(:conduct_apprenticeship_session)
11
+ expect(client).to respond_to(:recommend_apprenticeship_method)
12
+ expect(client).to respond_to(:graduated_apprenticeships)
13
+ expect(client).to respond_to(:active_apprenticeships)
14
+ expect(client).to respond_to(:mentor_apprenticeships)
15
+ expect(client).to respond_to(:apprentice_apprenticeships)
16
+ expect(client).to respond_to(:domain_apprenticeships)
17
+ expect(client).to respond_to(:update_cognitive_apprenticeship)
18
+ expect(client).to respond_to(:cognitive_apprenticeship_stats)
19
+ end
20
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipEngine do
4
+ let(:engine) { described_class.new }
5
+
6
+ let(:params) do
7
+ {
8
+ skill_name: 'terraform',
9
+ domain: 'infrastructure',
10
+ mentor_id: 'mentor-1',
11
+ apprentice_id: 'learner-1'
12
+ }
13
+ end
14
+
15
+ def create_one
16
+ engine.create_apprenticeship(**params)
17
+ end
18
+
19
+ describe '#create_apprenticeship' do
20
+ it 'returns a new Apprenticeship' do
21
+ appr = create_one
22
+ expect(appr).to be_a(Legion::Extensions::CognitiveApprenticeship::Helpers::Apprenticeship)
23
+ end
24
+
25
+ it 'stores the apprenticeship' do
26
+ appr = create_one
27
+ expect(engine.get(appr.id)).to eq(appr)
28
+ end
29
+
30
+ it 'increments count' do
31
+ expect { create_one }.to change(engine, :count).by(1)
32
+ end
33
+
34
+ it 'returns nil at capacity' do
35
+ stub_const('Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::MAX_APPRENTICESHIPS', 1)
36
+ create_one
37
+ expect(engine.create_apprenticeship(**params)).to be_nil
38
+ end
39
+ end
40
+
41
+ describe '#conduct_session' do
42
+ it 'returns the apprenticeship after learning' do
43
+ appr = create_one
44
+ result = engine.conduct_session(apprenticeship_id: appr.id, method: :modeling, success: true)
45
+ expect(result).to eq(appr)
46
+ end
47
+
48
+ it 'returns nil for unknown id' do
49
+ expect(engine.conduct_session(apprenticeship_id: 'nope', method: :modeling, success: true)).to be_nil
50
+ end
51
+
52
+ it 'records the session' do
53
+ appr = create_one
54
+ engine.conduct_session(apprenticeship_id: appr.id, method: :coaching, success: true)
55
+ expect(engine.sessions.size).to eq(1)
56
+ end
57
+ end
58
+
59
+ describe '#recommend_method' do
60
+ it 'returns the recommended method' do
61
+ appr = create_one
62
+ method = engine.recommend_method(apprenticeship_id: appr.id)
63
+ expect(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS).to include(method)
64
+ end
65
+
66
+ it 'returns nil for unknown id' do
67
+ expect(engine.recommend_method(apprenticeship_id: 'nope')).to be_nil
68
+ end
69
+ end
70
+
71
+ describe '#graduated_apprenticeships' do
72
+ it 'returns apprenticeships that have graduated' do
73
+ appr = create_one
74
+ 30.times { engine.conduct_session(apprenticeship_id: appr.id, method: :exploration, success: true) }
75
+ expect(engine.graduated_apprenticeships).to include(appr)
76
+ end
77
+
78
+ it 'excludes non-graduated apprenticeships' do
79
+ create_one
80
+ expect(engine.graduated_apprenticeships).to be_empty
81
+ end
82
+ end
83
+
84
+ describe '#active_apprenticeships' do
85
+ it 'returns non-graduated apprenticeships' do
86
+ create_one
87
+ expect(engine.active_apprenticeships.size).to eq(1)
88
+ end
89
+
90
+ it 'excludes graduated apprenticeships' do
91
+ appr = create_one
92
+ 30.times { engine.conduct_session(apprenticeship_id: appr.id, method: :exploration, success: true) }
93
+ expect(engine.active_apprenticeships).to be_empty
94
+ end
95
+ end
96
+
97
+ describe '#by_mentor' do
98
+ it 'returns apprenticeships for the given mentor' do
99
+ create_one
100
+ result = engine.by_mentor(mentor_id: 'mentor-1')
101
+ expect(result.size).to eq(1)
102
+ end
103
+
104
+ it 'returns empty for unknown mentor' do
105
+ create_one
106
+ expect(engine.by_mentor(mentor_id: 'nobody')).to be_empty
107
+ end
108
+ end
109
+
110
+ describe '#by_apprentice' do
111
+ it 'returns apprenticeships for the given apprentice' do
112
+ create_one
113
+ result = engine.by_apprentice(apprentice_id: 'learner-1')
114
+ expect(result.size).to eq(1)
115
+ end
116
+ end
117
+
118
+ describe '#by_domain' do
119
+ it 'returns apprenticeships in the given domain' do
120
+ create_one
121
+ result = engine.by_domain(domain: 'infrastructure')
122
+ expect(result.size).to eq(1)
123
+ end
124
+
125
+ it 'returns empty for unknown domain' do
126
+ create_one
127
+ expect(engine.by_domain(domain: 'unknown')).to be_empty
128
+ end
129
+ end
130
+
131
+ describe '#decay_all' do
132
+ it 'applies decay to all apprenticeships and returns count' do
133
+ create_one
134
+ expect(engine.decay_all).to eq(1)
135
+ end
136
+ end
137
+
138
+ describe '#to_h' do
139
+ it 'returns a summary hash' do
140
+ create_one
141
+ h = engine.to_h
142
+ expect(h).to include(:total, :active, :graduated, :sessions)
143
+ expect(h[:total]).to eq(1)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel do
4
+ subject(:model) { described_class }
5
+
6
+ describe 'constants' do
7
+ it 'defines METHODS with six items' do
8
+ expect(model::METHODS.size).to eq(6)
9
+ expect(model::METHODS).to include(:modeling, :coaching, :scaffolding, :articulation, :reflection, :exploration)
10
+ end
11
+
12
+ it 'defines MASTERY_THRESHOLD as 0.85' do
13
+ expect(model::MASTERY_THRESHOLD).to eq(0.85)
14
+ end
15
+
16
+ it 'defines LEARNING_GAIN as 0.08' do
17
+ expect(model::LEARNING_GAIN).to eq(0.08)
18
+ end
19
+
20
+ it 'defines EXPLORATION_MULTIPLIER as 2.0' do
21
+ expect(model::EXPLORATION_MULTIPLIER).to eq(2.0)
22
+ end
23
+
24
+ it 'defines COACHING_MULTIPLIER as 1.5' do
25
+ expect(model::COACHING_MULTIPLIER).to eq(1.5)
26
+ end
27
+
28
+ it 'defines DECAY_RATE as 0.01' do
29
+ expect(model::DECAY_RATE).to eq(0.01)
30
+ end
31
+ end
32
+
33
+ describe '.phase_for' do
34
+ it 'returns :modeling for mastery 0.0..0.19' do
35
+ expect(model.phase_for(0.0)).to eq(:modeling)
36
+ expect(model.phase_for(0.1)).to eq(:modeling)
37
+ end
38
+
39
+ it 'returns :coaching for mastery 0.2..0.39' do
40
+ expect(model.phase_for(0.2)).to eq(:coaching)
41
+ expect(model.phase_for(0.3)).to eq(:coaching)
42
+ end
43
+
44
+ it 'returns :scaffolding for mastery 0.4..0.59' do
45
+ expect(model.phase_for(0.4)).to eq(:scaffolding)
46
+ end
47
+
48
+ it 'returns :articulation for mastery 0.6..0.74' do
49
+ expect(model.phase_for(0.6)).to eq(:articulation)
50
+ end
51
+
52
+ it 'returns :reflection for mastery 0.75..0.84' do
53
+ expect(model.phase_for(0.75)).to eq(:reflection)
54
+ end
55
+
56
+ it 'returns :exploration for mastery >= 0.85' do
57
+ expect(model.phase_for(0.85)).to eq(:exploration)
58
+ expect(model.phase_for(1.0)).to eq(:exploration)
59
+ end
60
+ end
61
+
62
+ describe '.mastery_label_for' do
63
+ it 'returns :novice for mastery < 0.2' do
64
+ expect(model.mastery_label_for(0.1)).to eq(:novice)
65
+ end
66
+
67
+ it 'returns :apprentice for mastery 0.2..0.39' do
68
+ expect(model.mastery_label_for(0.3)).to eq(:apprentice)
69
+ end
70
+
71
+ it 'returns :intermediate for mastery 0.4..0.59' do
72
+ expect(model.mastery_label_for(0.5)).to eq(:intermediate)
73
+ end
74
+
75
+ it 'returns :proficient for mastery 0.6..0.79' do
76
+ expect(model.mastery_label_for(0.7)).to eq(:proficient)
77
+ end
78
+
79
+ it 'returns :expert for mastery >= 0.8' do
80
+ expect(model.mastery_label_for(0.9)).to eq(:expert)
81
+ end
82
+ end
83
+
84
+ describe '.clamp_mastery' do
85
+ it 'floors at MASTERY_FLOOR' do
86
+ expect(model.clamp_mastery(-0.5)).to eq(0.0)
87
+ end
88
+
89
+ it 'caps at MASTERY_CEILING' do
90
+ expect(model.clamp_mastery(1.5)).to eq(1.0)
91
+ end
92
+
93
+ it 'passes through valid values' do
94
+ expect(model.clamp_mastery(0.5)).to eq(0.5)
95
+ end
96
+ end
97
+
98
+ describe '.new_apprenticeship' do
99
+ let(:entry) do
100
+ model.new_apprenticeship(
101
+ skill_name: 'ruby',
102
+ domain: 'programming',
103
+ mentor_id: 'mentor-1',
104
+ apprentice_id: 'apprentice-1'
105
+ )
106
+ end
107
+
108
+ it 'sets default mastery' do
109
+ expect(entry[:mastery]).to eq(model::DEFAULT_MASTERY)
110
+ end
111
+
112
+ it 'sets current_phase from mastery' do
113
+ expect(entry[:current_phase]).to eq(model.phase_for(model::DEFAULT_MASTERY))
114
+ end
115
+
116
+ it 'generates a uuid id' do
117
+ expect(entry[:id]).to match(/\A[0-9a-f-]{36}\z/)
118
+ end
119
+
120
+ it 'sets session_count to 0' do
121
+ expect(entry[:session_count]).to eq(0)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::Apprenticeship do
4
+ let(:appr) do
5
+ described_class.new(
6
+ skill_name: 'ruby',
7
+ domain: 'programming',
8
+ mentor_id: 'mentor-1',
9
+ apprentice_id: 'apprentice-1'
10
+ )
11
+ end
12
+
13
+ describe '#initialize' do
14
+ it 'sets skill_name' do
15
+ expect(appr.skill_name).to eq('ruby')
16
+ end
17
+
18
+ it 'sets domain' do
19
+ expect(appr.domain).to eq('programming')
20
+ end
21
+
22
+ it 'starts at DEFAULT_MASTERY' do
23
+ expect(appr.mastery).to eq(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::DEFAULT_MASTERY)
24
+ end
25
+
26
+ it 'starts with zero sessions' do
27
+ expect(appr.session_count).to eq(0)
28
+ end
29
+
30
+ it 'generates a uuid' do
31
+ expect(appr.id).to match(/\A[0-9a-f-]{36}\z/)
32
+ end
33
+ end
34
+
35
+ describe '#current_phase' do
36
+ it 'returns :modeling at default mastery' do
37
+ expect(appr.current_phase).to eq(:modeling)
38
+ end
39
+ end
40
+
41
+ describe '#mastery_label' do
42
+ it 'returns :novice at default mastery' do
43
+ expect(appr.mastery_label).to eq(:novice)
44
+ end
45
+ end
46
+
47
+ describe '#graduated?' do
48
+ it 'returns false below threshold' do
49
+ expect(appr.graduated?).to be false
50
+ end
51
+
52
+ it 'returns true at threshold' do
53
+ 30.times { appr.learn!(method: :exploration, success: true) }
54
+ expect(appr.graduated?).to be true
55
+ end
56
+ end
57
+
58
+ describe '#recommended_method' do
59
+ it 'returns :modeling at default mastery' do
60
+ expect(appr.recommended_method).to eq(:modeling)
61
+ end
62
+ end
63
+
64
+ describe '#learn!' do
65
+ it 'increases mastery on success' do
66
+ before = appr.mastery
67
+ appr.learn!(method: :modeling, success: true)
68
+ expect(appr.mastery).to be > before
69
+ end
70
+
71
+ it 'does not increase mastery on failure' do
72
+ before = appr.mastery
73
+ appr.learn!(method: :modeling, success: false)
74
+ expect(appr.mastery).to eq(before)
75
+ end
76
+
77
+ it 'applies exploration multiplier' do
78
+ a1 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
79
+ a2 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
80
+ a1.learn!(method: :modeling, success: true)
81
+ a2.learn!(method: :exploration, success: true)
82
+ expect(a2.mastery).to be > a1.mastery
83
+ end
84
+
85
+ it 'applies coaching multiplier' do
86
+ a1 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
87
+ a2 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
88
+ a1.learn!(method: :modeling, success: true)
89
+ a2.learn!(method: :coaching, success: true)
90
+ expect(a2.mastery).to be > a1.mastery
91
+ end
92
+
93
+ it 'increments session_count' do
94
+ appr.learn!(method: :scaffolding, success: true)
95
+ expect(appr.session_count).to eq(1)
96
+ end
97
+
98
+ it 'clamps mastery at ceiling' do
99
+ 100.times { appr.learn!(method: :exploration, success: true) }
100
+ expect(appr.mastery).to be <= 1.0
101
+ end
102
+
103
+ it 'updates last_session_at' do
104
+ appr.learn!(method: :reflection, success: true)
105
+ expect(appr.last_session_at).not_to be_nil
106
+ end
107
+ end
108
+
109
+ describe '#decay!' do
110
+ it 'reduces mastery' do
111
+ appr.learn!(method: :exploration, success: true)
112
+ before = appr.mastery
113
+ appr.decay!
114
+ expect(appr.mastery).to be < before
115
+ end
116
+
117
+ it 'clamps at floor' do
118
+ 100.times { appr.decay! }
119
+ expect(appr.mastery).to be >= 0.0
120
+ end
121
+ end
122
+
123
+ describe '#to_h' do
124
+ it 'includes all expected keys' do
125
+ h = appr.to_h
126
+ expect(h).to include(:id, :skill_name, :domain, :mentor_id, :apprentice_id,
127
+ :mastery, :current_phase, :mastery_label, :graduated,
128
+ :session_count, :created_at, :last_session_at)
129
+ end
130
+
131
+ it 'reflects graduated status' do
132
+ 30.times { appr.learn!(method: :exploration, success: true) }
133
+ expect(appr.to_h[:graduated]).to be true
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_apprenticeship/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship::Runners::CognitiveApprenticeship do
6
+ let(:client) { Legion::Extensions::CognitiveApprenticeship::Client.new }
7
+
8
+ let(:valid_params) do
9
+ {
10
+ skill_name: 'ruby',
11
+ domain: 'programming',
12
+ mentor_id: 'mentor-1',
13
+ apprentice_id: 'learner-1'
14
+ }
15
+ end
16
+
17
+ def create_one
18
+ client.create_apprenticeship(**valid_params)
19
+ end
20
+
21
+ describe '#create_apprenticeship' do
22
+ it 'returns success: true with a valid apprenticeship hash' do
23
+ result = create_one
24
+ expect(result[:success]).to be true
25
+ expect(result[:apprenticeship]).to include(:id, :skill_name, :mastery)
26
+ end
27
+
28
+ it 'returns success: false when a param is too short' do
29
+ result = client.create_apprenticeship(skill_name: 'ab', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
30
+ expect(result[:success]).to be false
31
+ expect(result[:reason]).to eq(:param_too_short)
32
+ end
33
+ end
34
+
35
+ describe '#conduct_apprenticeship_session' do
36
+ it 'returns success: true and updated mastery' do
37
+ created = create_one
38
+ id = created[:apprenticeship][:id]
39
+ result = client.conduct_apprenticeship_session(apprenticeship_id: id, method: :modeling, success: true)
40
+ expect(result[:success]).to be true
41
+ expect(result[:apprenticeship][:mastery]).to be > 0.1
42
+ end
43
+
44
+ it 'returns success: false for unknown apprenticeship' do
45
+ result = client.conduct_apprenticeship_session(apprenticeship_id: 'nope', method: :modeling, success: true)
46
+ expect(result[:success]).to be false
47
+ expect(result[:reason]).to eq(:not_found)
48
+ end
49
+
50
+ it 'returns success: false for invalid method' do
51
+ created = create_one
52
+ id = created[:apprenticeship][:id]
53
+ result = client.conduct_apprenticeship_session(apprenticeship_id: id, method: :unknown, success: true)
54
+ expect(result[:success]).to be false
55
+ expect(result[:reason]).to eq(:invalid_method)
56
+ end
57
+
58
+ it 'accepts all six valid methods' do
59
+ Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS.each do |method|
60
+ c = Legion::Extensions::CognitiveApprenticeship::Client.new
61
+ created = c.create_apprenticeship(**valid_params)
62
+ id = created[:apprenticeship][:id]
63
+ result = c.conduct_apprenticeship_session(apprenticeship_id: id, method: method, success: true)
64
+ expect(result[:success]).to be true
65
+ end
66
+ end
67
+ end
68
+
69
+ describe '#recommend_apprenticeship_method' do
70
+ it 'returns success: true with a valid method' do
71
+ created = create_one
72
+ id = created[:apprenticeship][:id]
73
+ result = client.recommend_apprenticeship_method(apprenticeship_id: id)
74
+ expect(result[:success]).to be true
75
+ expect(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS).to include(result[:recommended_method])
76
+ end
77
+
78
+ it 'returns success: false for unknown apprenticeship' do
79
+ result = client.recommend_apprenticeship_method(apprenticeship_id: 'nope')
80
+ expect(result[:success]).to be false
81
+ end
82
+ end
83
+
84
+ describe '#graduated_apprenticeships' do
85
+ it 'returns success: true and an empty list initially' do
86
+ result = client.graduated_apprenticeships
87
+ expect(result[:success]).to be true
88
+ expect(result[:count]).to eq(0)
89
+ end
90
+
91
+ it 'includes graduated apprenticeships' do
92
+ created = create_one
93
+ id = created[:apprenticeship][:id]
94
+ 30.times { client.conduct_apprenticeship_session(apprenticeship_id: id, method: :exploration, success: true) }
95
+ result = client.graduated_apprenticeships
96
+ expect(result[:count]).to eq(1)
97
+ end
98
+ end
99
+
100
+ describe '#active_apprenticeships' do
101
+ it 'returns the newly created apprenticeship' do
102
+ create_one
103
+ result = client.active_apprenticeships
104
+ expect(result[:success]).to be true
105
+ expect(result[:count]).to eq(1)
106
+ end
107
+ end
108
+
109
+ describe '#mentor_apprenticeships' do
110
+ it 'returns apprenticeships for the mentor' do
111
+ create_one
112
+ result = client.mentor_apprenticeships(mentor_id: 'mentor-1')
113
+ expect(result[:success]).to be true
114
+ expect(result[:count]).to eq(1)
115
+ end
116
+ end
117
+
118
+ describe '#apprentice_apprenticeships' do
119
+ it 'returns apprenticeships for the apprentice' do
120
+ create_one
121
+ result = client.apprentice_apprenticeships(apprentice_id: 'learner-1')
122
+ expect(result[:success]).to be true
123
+ expect(result[:count]).to eq(1)
124
+ end
125
+ end
126
+
127
+ describe '#domain_apprenticeships' do
128
+ it 'returns apprenticeships for the domain' do
129
+ create_one
130
+ result = client.domain_apprenticeships(domain: 'programming')
131
+ expect(result[:success]).to be true
132
+ expect(result[:count]).to eq(1)
133
+ end
134
+ end
135
+
136
+ describe '#update_cognitive_apprenticeship' do
137
+ it 'delegates to conduct_apprenticeship_session' do
138
+ created = create_one
139
+ id = created[:apprenticeship][:id]
140
+ result = client.update_cognitive_apprenticeship(apprenticeship_id: id, method: :coaching, success: true)
141
+ expect(result[:success]).to be true
142
+ end
143
+ end
144
+
145
+ describe '#cognitive_apprenticeship_stats' do
146
+ it 'returns success: true with stats' do
147
+ create_one
148
+ result = client.cognitive_apprenticeship_stats
149
+ expect(result[:success]).to be true
150
+ expect(result[:total]).to eq(1)
151
+ expect(result).to include(:active, :graduated, :sessions)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveApprenticeship do
4
+ it 'has a version number' do
5
+ expect(Legion::Extensions::CognitiveApprenticeship::VERSION).not_to be_nil
6
+ end
7
+
8
+ it 'has a version that is a string' do
9
+ expect(Legion::Extensions::CognitiveApprenticeship::VERSION).to be_a(String)
10
+ end
11
+ end
@@ -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_apprenticeship'
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-apprenticeship
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: 'Collins'' Cognitive Apprenticeship model for brain-modeled agentic AI:
27
+ guided learning through modeling, coaching, scaffolding, articulation, reflection,
28
+ and exploration'
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - lex-cognitive-apprenticeship.gemspec
37
+ - lib/legion/extensions/cognitive_apprenticeship.rb
38
+ - lib/legion/extensions/cognitive_apprenticeship/client.rb
39
+ - lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship.rb
40
+ - lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine.rb
41
+ - lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model.rb
42
+ - lib/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship.rb
43
+ - lib/legion/extensions/cognitive_apprenticeship/version.rb
44
+ - spec/legion/extensions/cognitive_apprenticeship/client_spec.rb
45
+ - spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine_spec.rb
46
+ - spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model_spec.rb
47
+ - spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_spec.rb
48
+ - spec/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship_spec.rb
49
+ - spec/legion/extensions/cognitive_apprenticeship_spec.rb
50
+ - spec/spec_helper.rb
51
+ homepage: https://github.com/LegionIO/lex-cognitive-apprenticeship
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
56
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
57
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
58
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
59
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship/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 Apprenticeship
78
+ test_files: []