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.
- checksums.yaml +7 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/lex-mind-growth.gemspec +29 -0
- data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +55 -0
- data/lib/legion/extensions/mind_growth/client.rb +36 -0
- data/lib/legion/extensions/mind_growth/helpers/build_pipeline.rb +63 -0
- data/lib/legion/extensions/mind_growth/helpers/cognitive_models.rb +60 -0
- data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +69 -0
- data/lib/legion/extensions/mind_growth/helpers/constants.rb +55 -0
- data/lib/legion/extensions/mind_growth/helpers/fitness_evaluator.rb +58 -0
- data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +71 -0
- data/lib/legion/extensions/mind_growth/runners/analyzer.rb +52 -0
- data/lib/legion/extensions/mind_growth/runners/builder.rb +254 -0
- data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +178 -0
- data/lib/legion/extensions/mind_growth/runners/proposer.rb +269 -0
- data/lib/legion/extensions/mind_growth/runners/validator.rb +57 -0
- data/lib/legion/extensions/mind_growth/version.rb +9 -0
- data/lib/legion/extensions/mind_growth.rb +25 -0
- data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +81 -0
- data/spec/legion/extensions/mind_growth/client_spec.rb +111 -0
- data/spec/legion/extensions/mind_growth/helpers/build_pipeline_spec.rb +190 -0
- data/spec/legion/extensions/mind_growth/helpers/cognitive_models_spec.rb +106 -0
- data/spec/legion/extensions/mind_growth/helpers/concept_proposal_spec.rb +209 -0
- data/spec/legion/extensions/mind_growth/helpers/fitness_evaluator_spec.rb +123 -0
- data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +203 -0
- data/spec/legion/extensions/mind_growth/runners/analyzer_spec.rb +108 -0
- data/spec/legion/extensions/mind_growth/runners/builder_spec.rb +347 -0
- data/spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb +185 -0
- data/spec/legion/extensions/mind_growth/runners/proposer_spec.rb +493 -0
- data/spec/legion/extensions/mind_growth/runners/validator_spec.rb +120 -0
- data/spec/spec_helper.rb +22 -0
- 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,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
|