lex-mind-growth 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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +19 -0
  3. data/LICENSE +21 -0
  4. data/lex-mind-growth.gemspec +29 -0
  5. data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +55 -0
  6. data/lib/legion/extensions/mind_growth/client.rb +36 -0
  7. data/lib/legion/extensions/mind_growth/helpers/build_pipeline.rb +63 -0
  8. data/lib/legion/extensions/mind_growth/helpers/cognitive_models.rb +60 -0
  9. data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +69 -0
  10. data/lib/legion/extensions/mind_growth/helpers/constants.rb +55 -0
  11. data/lib/legion/extensions/mind_growth/helpers/fitness_evaluator.rb +58 -0
  12. data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +71 -0
  13. data/lib/legion/extensions/mind_growth/runners/analyzer.rb +52 -0
  14. data/lib/legion/extensions/mind_growth/runners/builder.rb +254 -0
  15. data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +178 -0
  16. data/lib/legion/extensions/mind_growth/runners/proposer.rb +269 -0
  17. data/lib/legion/extensions/mind_growth/runners/validator.rb +57 -0
  18. data/lib/legion/extensions/mind_growth/version.rb +9 -0
  19. data/lib/legion/extensions/mind_growth.rb +25 -0
  20. data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +81 -0
  21. data/spec/legion/extensions/mind_growth/client_spec.rb +111 -0
  22. data/spec/legion/extensions/mind_growth/helpers/build_pipeline_spec.rb +190 -0
  23. data/spec/legion/extensions/mind_growth/helpers/cognitive_models_spec.rb +106 -0
  24. data/spec/legion/extensions/mind_growth/helpers/concept_proposal_spec.rb +209 -0
  25. data/spec/legion/extensions/mind_growth/helpers/fitness_evaluator_spec.rb +123 -0
  26. data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +203 -0
  27. data/spec/legion/extensions/mind_growth/runners/analyzer_spec.rb +108 -0
  28. data/spec/legion/extensions/mind_growth/runners/builder_spec.rb +347 -0
  29. data/spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb +185 -0
  30. data/spec/legion/extensions/mind_growth/runners/proposer_spec.rb +493 -0
  31. data/spec/legion/extensions/mind_growth/runners/validator_spec.rb +120 -0
  32. data/spec/spec_helper.rb +22 -0
  33. metadata +91 -0
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module MindGrowth
8
+ module Runners
9
+ module Proposer
10
+ extend self
11
+
12
+ def analyze_gaps(existing_extensions: nil, **)
13
+ extensions = existing_extensions || current_extensions
14
+ analysis = Helpers::CognitiveModels.gap_analysis(extensions)
15
+ recommendations = Helpers::CognitiveModels.recommend_from_gaps(analysis)
16
+ Legion::Logging.debug "[mind_growth:proposer] gap analysis: #{recommendations.size} recommendations" if defined?(Legion::Logging)
17
+ { success: true, models: analysis, recommendations: recommendations.first(10) }
18
+ rescue ArgumentError => e
19
+ { success: false, error: e.message }
20
+ end
21
+
22
+ def propose_concept(category: nil, description: nil, name: nil, enrich: true, **)
23
+ cat = category&.to_sym || suggest_category
24
+ gem_name = name || "lex-#{SecureRandom.hex(4)}"
25
+ mod_name = derive_module_name(gem_name)
26
+ desc = description || "Proposed #{cat} extension"
27
+
28
+ redundancy = check_redundancy(gem_name, desc)
29
+ if redundancy[:redundant]
30
+ Legion::Logging.info "[mind_growth:proposer] rejected redundant: #{gem_name} (#{redundancy[:score]})" if defined?(Legion::Logging)
31
+ return { success: false, error: :redundant, similar_to: redundancy[:similar_to],
32
+ score: redundancy[:score] }
33
+ end
34
+
35
+ enrichment = enrich ? enrich_proposal(gem_name, cat, desc) : {}
36
+
37
+ proposal = Helpers::ConceptProposal.new(
38
+ name: gem_name,
39
+ module_name: mod_name,
40
+ category: cat,
41
+ description: desc,
42
+ helpers: enrichment[:helpers] || [],
43
+ runner_methods: enrichment[:runner_methods] || [],
44
+ metaphor: enrichment[:metaphor],
45
+ rationale: enrichment[:rationale],
46
+ origin: :manual
47
+ )
48
+ proposal_store.store(proposal)
49
+ Legion::Logging.info "[mind_growth:proposer] proposed: #{proposal.name} (#{cat})" if defined?(Legion::Logging)
50
+ { success: true, proposal: proposal.to_h }
51
+ rescue ArgumentError => e
52
+ { success: false, error: e.message }
53
+ end
54
+
55
+ def evaluate_proposal(proposal_id:, scores: nil, **)
56
+ proposal = proposal_store.get(proposal_id)
57
+ return { success: false, error: :not_found } unless proposal
58
+
59
+ eval_scores = scores || score_with_llm(proposal) || default_scores
60
+ proposal.evaluate!(eval_scores)
61
+ Legion::Logging.info "[mind_growth:proposer] evaluated #{proposal.name}: #{proposal.status}" if defined?(Legion::Logging)
62
+ { success: true, proposal: proposal.to_h, approved: proposal.status == :approved,
63
+ auto_approved: proposal.auto_approvable? }
64
+ rescue ArgumentError => e
65
+ { success: false, error: e.message }
66
+ end
67
+
68
+ def list_proposals(status: nil, limit: 20, **)
69
+ proposals = status ? proposal_store.by_status(status) : proposal_store.recent(limit: limit)
70
+ { success: true, proposals: proposals.map(&:to_h), count: proposals.size }
71
+ end
72
+
73
+ def proposal_stats(**)
74
+ { success: true, stats: proposal_store.stats }
75
+ end
76
+
77
+ def get_proposal_object(id)
78
+ proposal_store.get(id)
79
+ end
80
+
81
+ private
82
+
83
+ def proposal_store
84
+ @proposal_store ||= Helpers::ProposalStore.new
85
+ end
86
+
87
+ def current_extensions
88
+ if defined?(Legion::Extensions::Metacognition::Helpers::Constants::SUBSYSTEMS)
89
+ Legion::Extensions::Metacognition::Helpers::Constants::SUBSYSTEMS
90
+ else
91
+ []
92
+ end
93
+ end
94
+
95
+ def derive_module_name(gem_name)
96
+ gem_name.to_s.sub(/\Alex-/, '').split('-').map(&:capitalize).join
97
+ end
98
+
99
+ def suggest_category
100
+ target = Helpers::Constants::TARGET_DISTRIBUTION
101
+ actual = category_distribution
102
+ # Pick the category with the largest gap between target and actual proportion
103
+ target.max_by { |cat, pct| pct - (actual[cat] || 0.0) }[0]
104
+ end
105
+
106
+ def category_distribution
107
+ proposals = proposal_store.all
108
+ return {} if proposals.empty?
109
+
110
+ total = proposals.size.to_f
111
+ proposals.group_by(&:category).transform_values { |v| v.size / total }
112
+ end
113
+
114
+ def enrich_proposal(name, category, description)
115
+ return {} unless llm_available?
116
+
117
+ response = Legion::LLM.chat.ask(enrichment_prompt(name, category, description))
118
+ parse_enrichment(response.content)
119
+ rescue StandardError => e
120
+ Legion::Logging.debug "[mind_growth:proposer] LLM enrichment failed: #{e.message}" if defined?(Legion::Logging)
121
+ {}
122
+ end
123
+
124
+ def enrichment_prompt(name, category, description)
125
+ <<~PROMPT
126
+ Design a LegionIO cognitive extension called "#{name}" in the #{category} category.
127
+ Description: #{description}
128
+
129
+ Return a JSON object (no markdown fencing) with these keys:
130
+ - "metaphor": a one-sentence metaphor for how this extension works
131
+ - "rationale": why this extension is needed (one sentence)
132
+ - "helpers": array of objects with "name" (snake_case) and "methods" (array of objects with "name" and "params" array)
133
+ - "runner_methods": array of objects with "name" (snake_case), "params" (array of strings), and "returns" (description string)
134
+
135
+ Keep it focused: 1-2 helpers, 2-4 runner methods. Use in-memory state patterns.
136
+ PROMPT
137
+ end
138
+
139
+ def parse_enrichment(content)
140
+ cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
141
+ data = ::JSON.parse(cleaned, symbolize_names: true)
142
+ {
143
+ metaphor: data[:metaphor],
144
+ rationale: data[:rationale],
145
+ helpers: Array(data[:helpers]).map { |h| { name: h[:name].to_s, methods: Array(h[:methods]) } },
146
+ runner_methods: Array(data[:runner_methods]).map do |r|
147
+ { name: r[:name].to_s, params: Array(r[:params]).map(&:to_s), returns: r[:returns].to_s }
148
+ end
149
+ }
150
+ rescue ::JSON::ParserError, NoMethodError
151
+ {}
152
+ end
153
+
154
+ def llm_available?
155
+ defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
156
+ end
157
+
158
+ def score_with_llm(proposal)
159
+ return nil unless llm_available?
160
+
161
+ response = Legion::LLM.chat.ask(scoring_prompt(proposal))
162
+ parse_scores(response.content)
163
+ rescue StandardError => e
164
+ Legion::Logging.debug "[mind_growth:proposer] LLM scoring failed: #{e.message}" if defined?(Legion::Logging)
165
+ nil
166
+ end
167
+
168
+ def scoring_prompt(proposal)
169
+ <<~PROMPT
170
+ Score this proposed LegionIO cognitive extension on five dimensions.
171
+ Each score must be a float between 0.0 and 1.0.
172
+
173
+ Extension: #{proposal.name}
174
+ Category: #{proposal.category}
175
+ Description: #{proposal.description}
176
+ #{"Metaphor: #{proposal.metaphor}" if proposal.metaphor}
177
+ #{"Rationale: #{proposal.rationale}" if proposal.rationale}
178
+ Helpers: #{proposal.helpers.map { |h| h[:name] }.join(', ').then { |s| s.empty? ? 'none' : s }}
179
+ Runner methods: #{proposal.runner_methods.map { |r| r[:name] }.join(', ').then { |s| s.empty? ? 'none' : s }}
180
+
181
+ Scoring dimensions:
182
+ - novelty: How unique is this extension relative to existing cognitive capabilities?
183
+ - fit: How well does it fill a gap in the current extension ecosystem?
184
+ - cognitive_value: How much does it add to the cognitive architecture?
185
+ - implementability: How feasible is it to implement with current infrastructure?
186
+ - composability: How well does it compose with other extensions?
187
+
188
+ Return ONLY a JSON object (no markdown fencing) with these five keys and float values:
189
+ {"novelty": 0.0, "fit": 0.0, "cognitive_value": 0.0, "implementability": 0.0, "composability": 0.0}
190
+ PROMPT
191
+ end
192
+
193
+ def parse_scores(content)
194
+ cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
195
+ data = ::JSON.parse(cleaned, symbolize_names: true)
196
+ Helpers::Constants::EVALUATION_DIMENSIONS.to_h do |dim|
197
+ val = data[dim]
198
+ return nil unless val.is_a?(Numeric)
199
+
200
+ [dim, val.to_f.clamp(0.0, 1.0)]
201
+ end
202
+ rescue ::JSON::ParserError, NoMethodError
203
+ nil
204
+ end
205
+
206
+ def default_scores
207
+ Helpers::Constants::EVALUATION_DIMENSIONS.to_h { |d| [d, 0.7] }
208
+ end
209
+
210
+ def check_redundancy(name, description)
211
+ existing = proposal_store.all
212
+ return { redundant: false } if existing.empty?
213
+
214
+ exact = existing.find { |p| p.name == name }
215
+ return { redundant: true, similar_to: exact.name, score: 1.0 } if exact
216
+
217
+ check_redundancy_with_llm(name, description, existing) || { redundant: false }
218
+ end
219
+
220
+ def check_redundancy_with_llm(name, description, existing)
221
+ return nil unless llm_available?
222
+
223
+ candidates = existing.last(20).map { |p| { name: p.name, description: p.description } }
224
+ response = Legion::LLM.chat.ask(redundancy_prompt(name, description, candidates))
225
+ parse_redundancy(response.content)
226
+ rescue StandardError => e
227
+ Legion::Logging.debug "[mind_growth:proposer] LLM redundancy check failed: #{e.message}" if defined?(Legion::Logging)
228
+ nil
229
+ end
230
+
231
+ def redundancy_prompt(name, description, candidates)
232
+ list = candidates.map { |c| "- #{c[:name]}: #{c[:description]}" }.join("\n")
233
+ <<~PROMPT
234
+ Determine if a proposed extension is redundant with any existing proposal.
235
+
236
+ Proposed extension:
237
+ - Name: #{name}
238
+ - Description: #{description}
239
+
240
+ Existing proposals:
241
+ #{list}
242
+
243
+ Return ONLY a JSON object (no markdown fencing) with:
244
+ - "redundant": true/false (true if the proposed extension substantially overlaps an existing one)
245
+ - "similar_to": name of the most similar existing proposal (or null if not redundant)
246
+ - "score": float 0.0-1.0 measuring semantic similarity (>= 0.8 means redundant)
247
+ PROMPT
248
+ end
249
+
250
+ def parse_redundancy(content)
251
+ cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
252
+ data = ::JSON.parse(cleaned, symbolize_names: true)
253
+ score = data[:score]
254
+ return nil unless score.is_a?(Numeric)
255
+
256
+ score = score.to_f.clamp(0.0, 1.0)
257
+ {
258
+ redundant: score >= Helpers::Constants::REDUNDANCY_THRESHOLD,
259
+ similar_to: data[:similar_to],
260
+ score: score
261
+ }
262
+ rescue ::JSON::ParserError, NoMethodError
263
+ nil
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module Validator
8
+ extend self
9
+
10
+ def validate_proposal(proposal_id:, **)
11
+ proposal = find_proposal(proposal_id)
12
+ return { success: false, error: :not_found } unless proposal
13
+
14
+ issues = []
15
+ issues << 'missing name' if proposal.name.nil? || proposal.name.empty?
16
+ issues << 'missing module_name' if proposal.module_name.nil? || proposal.module_name.empty?
17
+ issues << 'missing category' unless Helpers::Constants::CATEGORIES.include?(proposal.category&.to_sym)
18
+ issues << 'missing description' if proposal.description.nil? || proposal.description.empty?
19
+
20
+ { success: issues.empty?, valid: issues.empty?, issues: issues, proposal_id: proposal_id }
21
+ rescue ArgumentError => e
22
+ { success: false, error: e.message }
23
+ end
24
+
25
+ def validate_scores(scores:, **)
26
+ issues = []
27
+ Helpers::Constants::EVALUATION_DIMENSIONS.each do |dim|
28
+ value = scores[dim]
29
+ issues << "missing #{dim}" if value.nil?
30
+ issues << "#{dim} out of range (#{value})" if value && (value < 0.0 || value > 1.0)
31
+ end
32
+ { success: issues.empty?, valid: issues.empty?, issues: issues }
33
+ rescue ArgumentError => e
34
+ { success: false, error: e.message }
35
+ end
36
+
37
+ def validate_fitness(extensions:, **)
38
+ ranked = Helpers::FitnessEvaluator.rank(extensions)
39
+ prune = Helpers::FitnessEvaluator.prune_candidates(extensions)
40
+ improve = Helpers::FitnessEvaluator.improvement_candidates(extensions)
41
+ { success: true, ranked: ranked, prune_candidates: prune.size, improvement_candidates: improve.size }
42
+ rescue ArgumentError => e
43
+ { success: false, error: e.message }
44
+ end
45
+
46
+ private
47
+
48
+ def find_proposal(proposal_id)
49
+ return nil unless defined?(Runners::Proposer) && Runners::Proposer.respond_to?(:get_proposal_object)
50
+
51
+ Runners::Proposer.get_proposal_object(proposal_id)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require 'legion/extensions/mind_growth/version'
6
+ require 'legion/extensions/mind_growth/helpers/constants'
7
+ require 'legion/extensions/mind_growth/helpers/concept_proposal'
8
+ require 'legion/extensions/mind_growth/helpers/proposal_store'
9
+ require 'legion/extensions/mind_growth/helpers/cognitive_models'
10
+ require 'legion/extensions/mind_growth/helpers/build_pipeline'
11
+ require 'legion/extensions/mind_growth/helpers/fitness_evaluator'
12
+ require 'legion/extensions/mind_growth/runners/proposer'
13
+ require 'legion/extensions/mind_growth/runners/analyzer'
14
+ require 'legion/extensions/mind_growth/runners/builder'
15
+ require 'legion/extensions/mind_growth/runners/validator'
16
+ require 'legion/extensions/mind_growth/runners/orchestrator'
17
+ require 'legion/extensions/mind_growth/client'
18
+
19
+ module Legion
20
+ module Extensions
21
+ module MindGrowth
22
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ module Legion
5
+ module Extensions
6
+ module Actors
7
+ class Every # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ # Intercept the require in the actor file so it doesn't fail
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
15
+
16
+ require 'legion/extensions/mind_growth/actors/growth_cycle'
17
+
18
+ RSpec.describe Legion::Extensions::MindGrowth::Actor::GrowthCycle do
19
+ subject(:actor) { described_class.new }
20
+
21
+ describe '#runner_class' do
22
+ it 'returns the Orchestrator module' do
23
+ expect(actor.runner_class).to eq(Legion::Extensions::MindGrowth::Runners::Orchestrator)
24
+ end
25
+ end
26
+
27
+ describe '#runner_function' do
28
+ it 'returns run_growth_cycle' do
29
+ expect(actor.runner_function).to eq('run_growth_cycle')
30
+ end
31
+ end
32
+
33
+ describe '#time' do
34
+ it 'returns 3600 seconds (1 hour)' do
35
+ expect(actor.time).to eq(3600)
36
+ end
37
+ end
38
+
39
+ describe '#enabled?' do
40
+ it 'returns false when neither codegen nor exec is loaded' do
41
+ hide_const('Legion::Extensions::Codegen') if defined?(Legion::Extensions::Codegen)
42
+ hide_const('Legion::Extensions::Exec') if defined?(Legion::Extensions::Exec)
43
+ expect(actor.enabled?).to be_falsey
44
+ end
45
+
46
+ it 'returns truthy when codegen is loaded' do
47
+ stub_const('Legion::Extensions::Codegen', Module.new)
48
+ expect(actor.enabled?).to be_truthy
49
+ end
50
+
51
+ it 'returns truthy when exec is loaded' do
52
+ hide_const('Legion::Extensions::Codegen') if defined?(Legion::Extensions::Codegen)
53
+ stub_const('Legion::Extensions::Exec', Module.new)
54
+ expect(actor.enabled?).to be_truthy
55
+ end
56
+ end
57
+
58
+ describe '#run_now?' do
59
+ it 'returns false' do
60
+ expect(actor.run_now?).to be false
61
+ end
62
+ end
63
+
64
+ describe '#use_runner?' do
65
+ it 'returns false' do
66
+ expect(actor.use_runner?).to be false
67
+ end
68
+ end
69
+
70
+ describe '#check_subtask?' do
71
+ it 'returns false' do
72
+ expect(actor.check_subtask?).to be false
73
+ end
74
+ end
75
+
76
+ describe '#generate_task?' do
77
+ it 'returns false' do
78
+ expect(actor.generate_task?).to be false
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MindGrowth::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ before { Legion::Extensions::MindGrowth::Runners::Proposer.instance_variable_set(:@proposal_store, nil) }
7
+
8
+ describe '#initialize' do
9
+ it 'instantiates without arguments' do
10
+ expect { described_class.new }.not_to raise_error
11
+ end
12
+ end
13
+
14
+ describe 'Proposer methods' do
15
+ it 'responds to analyze_gaps' do
16
+ expect(client).to respond_to(:analyze_gaps)
17
+ end
18
+
19
+ it 'responds to propose_concept' do
20
+ expect(client).to respond_to(:propose_concept)
21
+ end
22
+
23
+ it 'responds to evaluate_proposal' do
24
+ expect(client).to respond_to(:evaluate_proposal)
25
+ end
26
+
27
+ it 'responds to list_proposals' do
28
+ expect(client).to respond_to(:list_proposals)
29
+ end
30
+
31
+ it 'responds to proposal_stats' do
32
+ expect(client).to respond_to(:proposal_stats)
33
+ end
34
+ end
35
+
36
+ describe 'Analyzer methods' do
37
+ it 'responds to cognitive_profile' do
38
+ expect(client).to respond_to(:cognitive_profile)
39
+ end
40
+
41
+ it 'responds to identify_weak_links' do
42
+ expect(client).to respond_to(:identify_weak_links)
43
+ end
44
+
45
+ it 'responds to recommend_priorities' do
46
+ expect(client).to respond_to(:recommend_priorities)
47
+ end
48
+ end
49
+
50
+ describe 'Builder methods' do
51
+ it 'responds to build_extension' do
52
+ expect(client).to respond_to(:build_extension)
53
+ end
54
+
55
+ it 'responds to build_status' do
56
+ expect(client).to respond_to(:build_status)
57
+ end
58
+ end
59
+
60
+ describe 'Validator methods' do
61
+ it 'responds to validate_proposal' do
62
+ expect(client).to respond_to(:validate_proposal)
63
+ end
64
+
65
+ it 'responds to validate_scores' do
66
+ expect(client).to respond_to(:validate_scores)
67
+ end
68
+
69
+ it 'responds to validate_fitness' do
70
+ expect(client).to respond_to(:validate_fitness)
71
+ end
72
+ end
73
+
74
+ describe 'end-to-end workflow' do
75
+ it 'proposes, evaluates, and validates a concept' do
76
+ propose_result = client.propose_concept(name: 'lex-e2e', category: :cognition, description: 'end-to-end test')
77
+ expect(propose_result[:success]).to be true
78
+
79
+ proposal_id = propose_result[:proposal][:id]
80
+
81
+ scores = Legion::Extensions::MindGrowth::Helpers::Constants::EVALUATION_DIMENSIONS.to_h { |d| [d, 0.8] }
82
+ evaluate_result = client.evaluate_proposal(proposal_id: proposal_id, scores: scores)
83
+ expect(evaluate_result[:approved]).to be true
84
+
85
+ validate_result = client.validate_proposal(proposal_id: proposal_id)
86
+ expect(validate_result[:valid]).to be true
87
+ end
88
+
89
+ it 'provides gap analysis and prioritized recommendations' do
90
+ profile = client.cognitive_profile(existing_extensions: %i[attention memory])
91
+ expect(profile[:overall_coverage]).to be < 1.0
92
+
93
+ priorities = client.recommend_priorities(existing_extensions: %i[attention memory])
94
+ expect(priorities[:priorities]).to be_an(Array)
95
+ end
96
+
97
+ it 'builds a proposal end-to-end' do
98
+ propose_result = client.propose_concept(name: 'lex-build-e2e', category: :safety, description: 'build e2e')
99
+ proposal_id = propose_result[:proposal][:id]
100
+ build_result = client.build_extension(proposal_id: proposal_id)
101
+ expect(build_result[:success]).to be true
102
+ expect(build_result[:pipeline][:stage]).to eq(:complete)
103
+ end
104
+
105
+ it 'validates scores before evaluation' do
106
+ valid_scores = Legion::Extensions::MindGrowth::Helpers::Constants::EVALUATION_DIMENSIONS.to_h { |d| [d, 0.7] }
107
+ validation = client.validate_scores(scores: valid_scores)
108
+ expect(validation[:valid]).to be true
109
+ end
110
+ end
111
+ end