lex-mind-growth 0.1.8 → 0.1.9
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 +4 -4
- data/lib/legion/extensions/mind_growth/client.rb +16 -0
- data/lib/legion/extensions/mind_growth/runners/dashboard.rb +104 -0
- data/lib/legion/extensions/mind_growth/runners/evolver.rb +169 -0
- data/lib/legion/extensions/mind_growth/version.rb +1 -1
- data/lib/legion/extensions/mind_growth.rb +2 -0
- data/spec/legion/extensions/mind_growth/runners/dashboard_spec.rb +350 -0
- data/spec/legion/extensions/mind_growth/runners/evolver_spec.rb +357 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 647d99aa35555ba0ff36f584ad78d0c2104a9c09e83d72fbee7b897d097c7380
|
|
4
|
+
data.tar.gz: b7803d7ea950fb03154b695b5631f8a2e523adfad0df4963f8a22bf98e952f67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2874a80b17163ad6b0d9f92f6ce221b22e4bbecf317c779315279f5a7c4426154f342892c7765cf0d7e29f55941e9561d28a1d2d92cee5d7298151d5d47fc4c
|
|
7
|
+
data.tar.gz: 01c677bf65ca0c841f5740a7d1f89a41d160e1bb8bcd39f43ad247b4676a11d2c55bb67d4eaad7ed32c73e8838e1e1064839d0e4e461897b5a8af4c8c36945ca
|
|
@@ -68,6 +68,22 @@ module Legion
|
|
|
68
68
|
def generate_dream_proposals(**) = Runners::DreamIdeation.generate_dream_proposals(**)
|
|
69
69
|
def dream_agenda_items(**) = Runners::DreamIdeation.dream_agenda_items(**)
|
|
70
70
|
def enrich_from_dream_context(**) = Runners::DreamIdeation.enrich_from_dream_context(**)
|
|
71
|
+
|
|
72
|
+
# Evolver delegation
|
|
73
|
+
def select_for_improvement(**) = Runners::Evolver.select_for_improvement(**)
|
|
74
|
+
def propose_improvement(**) = Runners::Evolver.propose_improvement(**)
|
|
75
|
+
def replace_extension(**) = Runners::Evolver.replace_extension(**)
|
|
76
|
+
def merge_extensions(**) = Runners::Evolver.merge_extensions(**)
|
|
77
|
+
def evolution_summary(**) = Runners::Evolver.evolution_summary(**)
|
|
78
|
+
|
|
79
|
+
# Dashboard delegation
|
|
80
|
+
def extension_timeline(**) = Runners::Dashboard.extension_timeline(**)
|
|
81
|
+
def category_distribution(**) = Runners::Dashboard.category_distribution(**)
|
|
82
|
+
def build_metrics(**) = Runners::Dashboard.build_metrics(**)
|
|
83
|
+
def top_extensions(**) = Runners::Dashboard.top_extensions(**)
|
|
84
|
+
def bottom_extensions(**) = Runners::Dashboard.bottom_extensions(**)
|
|
85
|
+
def recent_proposals(**) = Runners::Dashboard.recent_proposals(**)
|
|
86
|
+
def full_dashboard(**) = Runners::Dashboard.full_dashboard(**)
|
|
71
87
|
end
|
|
72
88
|
end
|
|
73
89
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module Dashboard
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def extension_timeline(extensions:, days: 30, **)
|
|
14
|
+
count = Array(extensions).size
|
|
15
|
+
today = Time.now.utc.strftime('%Y-%m-%d')
|
|
16
|
+
series = [{ date: today, count: count }]
|
|
17
|
+
|
|
18
|
+
{ success: true, series: series, range_days: days }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def category_distribution(extensions:, **)
|
|
22
|
+
exts = Array(extensions)
|
|
23
|
+
dist = Helpers::Constants::CATEGORIES.to_h { |c| [c, 0] }
|
|
24
|
+
|
|
25
|
+
exts.each do |ext|
|
|
26
|
+
cat = (ext[:category] || :cognition).to_sym
|
|
27
|
+
dist[cat] = (dist[cat] || 0) + 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{ success: true, distribution: dist, total: exts.size }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_metrics(**)
|
|
34
|
+
stats = Runners::Proposer.proposal_stats[:stats]
|
|
35
|
+
by_status = stats[:by_status] || {}
|
|
36
|
+
total = stats[:total] || 0
|
|
37
|
+
|
|
38
|
+
approved = by_status[:approved].to_i
|
|
39
|
+
rejected = by_status[:rejected].to_i
|
|
40
|
+
built = (by_status[:passing].to_i + by_status[:wired].to_i + by_status[:active].to_i)
|
|
41
|
+
failed = by_status[:build_failed].to_i
|
|
42
|
+
|
|
43
|
+
attempted = built + failed
|
|
44
|
+
success_rate = attempted.positive? ? (built.to_f / attempted).round(3) : 0.0
|
|
45
|
+
|
|
46
|
+
evaluated = approved + rejected
|
|
47
|
+
approval_rate = evaluated.positive? ? (approved.to_f / evaluated).round(3) : 0.0
|
|
48
|
+
|
|
49
|
+
{ success: true,
|
|
50
|
+
total_proposals: total,
|
|
51
|
+
approved: approved,
|
|
52
|
+
rejected: rejected,
|
|
53
|
+
built: built,
|
|
54
|
+
failed: failed,
|
|
55
|
+
success_rate: success_rate,
|
|
56
|
+
approval_rate: approval_rate }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def top_extensions(extensions:, limit: 10, **)
|
|
60
|
+
exts = Array(extensions)
|
|
61
|
+
ranked = Helpers::FitnessEvaluator.rank(exts)
|
|
62
|
+
top = ranked.first(limit).map do |e|
|
|
63
|
+
{ name: e[:name] || e[:extension_name],
|
|
64
|
+
invocation_count: e[:invocation_count] || 0,
|
|
65
|
+
fitness: e[:fitness] }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
{ success: true, top: top, limit: limit }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def bottom_extensions(extensions:, limit: 10, **)
|
|
72
|
+
exts = Array(extensions)
|
|
73
|
+
ranked = Helpers::FitnessEvaluator.rank(exts)
|
|
74
|
+
bottom = ranked.last(limit).reverse.map do |e|
|
|
75
|
+
{ name: e[:name] || e[:extension_name],
|
|
76
|
+
invocation_count: e[:invocation_count] || 0,
|
|
77
|
+
fitness: e[:fitness] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
{ success: true, bottom: bottom, limit: limit }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def recent_proposals(limit: 10, **)
|
|
84
|
+
result = Runners::Proposer.list_proposals(limit: limit)
|
|
85
|
+
{ success: true, proposals: result[:proposals], count: result[:count] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def full_dashboard(extensions:, **)
|
|
89
|
+
exts = Array(extensions)
|
|
90
|
+
|
|
91
|
+
{ success: true,
|
|
92
|
+
category_distribution: category_distribution(extensions: exts)[:distribution],
|
|
93
|
+
build_metrics: build_metrics,
|
|
94
|
+
top_extensions: top_extensions(extensions: exts)[:top],
|
|
95
|
+
bottom_extensions: bottom_extensions(extensions: exts)[:bottom],
|
|
96
|
+
recent_proposals: recent_proposals[:proposals],
|
|
97
|
+
health_summary: Runners::Monitor.health_summary(extensions: exts),
|
|
98
|
+
timestamp: Time.now.utc.iso8601 }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module Evolver
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
BOTTOM_PERCENTILE = 0.05
|
|
14
|
+
SPECIATION_DRIFT_THRESHOLD = 0.5
|
|
15
|
+
|
|
16
|
+
def select_for_improvement(extensions:, count: 3, **)
|
|
17
|
+
exts = Array(extensions)
|
|
18
|
+
return { success: true, candidates: [], count: 0, total_evaluated: 0 } if exts.empty?
|
|
19
|
+
|
|
20
|
+
eligible = exts.reject { |e| %i[building testing].include?((e[:status] || :active).to_sym) }
|
|
21
|
+
ranked = Helpers::FitnessEvaluator.rank(eligible)
|
|
22
|
+
bottom_n = ranked.last(count)
|
|
23
|
+
|
|
24
|
+
{ success: true, candidates: bottom_n, count: bottom_n.size, total_evaluated: eligible.size }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def propose_improvement(extension:, **)
|
|
28
|
+
name = extension[:name] || extension[:extension_name]
|
|
29
|
+
fitness = Helpers::FitnessEvaluator.fitness(extension)
|
|
30
|
+
|
|
31
|
+
weaknesses = identify_weaknesses(extension)
|
|
32
|
+
suggestions = generate_suggestions(weaknesses)
|
|
33
|
+
|
|
34
|
+
if defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
|
|
35
|
+
suggestions = llm_suggestions(name, fitness, weaknesses) || suggestions
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ success: true, extension_name: name, fitness: fitness,
|
|
39
|
+
weaknesses: weaknesses, suggestions: suggestions }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def replace_extension(old_name:, new_proposal_id:, **)
|
|
43
|
+
status_store[old_name] = :pruned
|
|
44
|
+
replacement_map[old_name] = new_proposal_id
|
|
45
|
+
|
|
46
|
+
{ success: true, replaced: old_name, replacement_proposal_id: new_proposal_id }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge_extensions(extension_a:, extension_b:, merged_name: nil, **)
|
|
50
|
+
name_a = extension_a[:name] || extension_a[:extension_name]
|
|
51
|
+
name_b = extension_b[:name] || extension_b[:extension_name]
|
|
52
|
+
cat_a = (extension_a[:category] || :cognition).to_sym
|
|
53
|
+
merged = merged_name || "lex-merged-#{name_a.to_s.sub(/\Alex-/, '')}-#{name_b.to_s.sub(/\Alex-/, '')}"
|
|
54
|
+
desc = "Merged extension combining capabilities of #{name_a} and #{name_b}"
|
|
55
|
+
|
|
56
|
+
proposal = Runners::Proposer.propose_concept(
|
|
57
|
+
name: merged,
|
|
58
|
+
category: cat_a,
|
|
59
|
+
description: desc,
|
|
60
|
+
enrich: false
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
{ success: true, merged_proposal: proposal, sources: [name_a, name_b] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def evolution_summary(extensions:, **)
|
|
67
|
+
exts = Array(extensions)
|
|
68
|
+
|
|
69
|
+
improvement_candidates = select_for_improvement(extensions: exts, count: 5)[:candidates]
|
|
70
|
+
|
|
71
|
+
prune_candidates = Helpers::FitnessEvaluator.prune_candidates(exts).map do |e|
|
|
72
|
+
e[:name] || e[:extension_name]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
speciation_candidates = exts.filter_map do |e|
|
|
76
|
+
e[:name] || e[:extension_name] if (e[:drift_score] || 0.0) >= SPECIATION_DRIFT_THRESHOLD
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
fitnesses = exts.map { |e| Helpers::FitnessEvaluator.fitness(e) }
|
|
80
|
+
|
|
81
|
+
distribution = if fitnesses.empty?
|
|
82
|
+
{ min: 0.0, max: 0.0, mean: 0.0, median: 0.0 }
|
|
83
|
+
else
|
|
84
|
+
sorted = fitnesses.sort
|
|
85
|
+
mid = sorted.size / 2
|
|
86
|
+
median = sorted.size.odd? ? sorted[mid] : ((sorted[mid - 1] + sorted[mid]) / 2.0).round(3)
|
|
87
|
+
{ min: sorted.first.round(3),
|
|
88
|
+
max: sorted.last.round(3),
|
|
89
|
+
mean: (fitnesses.sum / fitnesses.size.to_f).round(3),
|
|
90
|
+
median: median }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
{ success: true,
|
|
94
|
+
improvement_candidates: improvement_candidates,
|
|
95
|
+
prune_candidates: prune_candidates,
|
|
96
|
+
speciation_candidates: speciation_candidates,
|
|
97
|
+
fitness_distribution: distribution }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
SUGGESTION_MAP = {
|
|
101
|
+
low_invocations: 'improve wiring or broaden phase coverage',
|
|
102
|
+
high_error_rate: 'add error handling and input validation',
|
|
103
|
+
high_latency: 'optimize hot paths or add caching',
|
|
104
|
+
low_impact: 'enrich output or add downstream connections'
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def identify_weaknesses(extension)
|
|
110
|
+
weaknesses = []
|
|
111
|
+
count = extension[:invocation_count] || 0
|
|
112
|
+
error = extension[:error_rate] || 0.0
|
|
113
|
+
lat = extension[:avg_latency_ms] || 0
|
|
114
|
+
imp = extension[:impact_score] || 0.5
|
|
115
|
+
|
|
116
|
+
weaknesses << :low_invocations if count < Helpers::Constants::DECAY_INVOCATION_THRESHOLD
|
|
117
|
+
weaknesses << :high_error_rate if error > 0.2
|
|
118
|
+
weaknesses << :high_latency if lat > 1000
|
|
119
|
+
weaknesses << :low_impact if imp < 0.3
|
|
120
|
+
weaknesses
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def generate_suggestions(weaknesses)
|
|
124
|
+
suggestions = weaknesses.filter_map { |w| SUGGESTION_MAP[w] }
|
|
125
|
+
suggestions.empty? ? ['review overall design for incremental improvements'] : suggestions
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def llm_suggestions(name, fitness, weaknesses)
|
|
129
|
+
response = Legion::LLM.chat(
|
|
130
|
+
caller: { extension: 'lex-mind-growth', operation: 'evolver', phase: 'suggest' }
|
|
131
|
+
).ask(improvement_prompt(name, fitness, weaknesses))
|
|
132
|
+
parse_llm_suggestions(response.content)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def improvement_prompt(name, fitness, weaknesses)
|
|
138
|
+
<<~PROMPT
|
|
139
|
+
The LegionIO cognitive extension "#{name}" has a fitness score of #{fitness.round(3)}.
|
|
140
|
+
Identified weaknesses: #{weaknesses.join(', ')}.
|
|
141
|
+
|
|
142
|
+
Provide 2-4 concrete improvement suggestions as a JSON array of strings.
|
|
143
|
+
Example: ["suggestion one", "suggestion two"]
|
|
144
|
+
Return ONLY the JSON array, no markdown fencing.
|
|
145
|
+
PROMPT
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_llm_suggestions(content)
|
|
149
|
+
cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
|
|
150
|
+
data = ::JSON.parse(cleaned)
|
|
151
|
+
return nil unless data.is_a?(Array)
|
|
152
|
+
|
|
153
|
+
data.map(&:to_s).reject(&:empty?)
|
|
154
|
+
rescue ::JSON::ParserError
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def status_store
|
|
159
|
+
@status_store ||= {}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def replacement_map
|
|
163
|
+
@replacement_map ||= {}
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -24,6 +24,8 @@ require 'legion/extensions/mind_growth/helpers/composition_map'
|
|
|
24
24
|
require 'legion/extensions/mind_growth/runners/monitor'
|
|
25
25
|
require 'legion/extensions/mind_growth/runners/composer'
|
|
26
26
|
require 'legion/extensions/mind_growth/runners/dream_ideation'
|
|
27
|
+
require 'legion/extensions/mind_growth/runners/evolver'
|
|
28
|
+
require 'legion/extensions/mind_growth/runners/dashboard'
|
|
27
29
|
require 'legion/extensions/mind_growth/client'
|
|
28
30
|
|
|
29
31
|
module Legion
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MindGrowth::Runners::Dashboard do
|
|
4
|
+
subject(:dashboard) { described_class }
|
|
5
|
+
|
|
6
|
+
before { Legion::Extensions::MindGrowth::Runners::Proposer.instance_variable_set(:@proposal_store, nil) }
|
|
7
|
+
|
|
8
|
+
let(:cognition_ext) do
|
|
9
|
+
{ name: 'lex-cognition', category: :cognition, invocation_count: 500,
|
|
10
|
+
impact_score: 0.8, health_score: 0.9, error_rate: 0.0, avg_latency_ms: 100 }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:memory_ext) do
|
|
14
|
+
{ name: 'lex-memory', category: :memory, invocation_count: 200,
|
|
15
|
+
impact_score: 0.6, health_score: 0.7, error_rate: 0.05, avg_latency_ms: 150 }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:low_ext) do
|
|
19
|
+
{ name: 'lex-low', category: :perception, invocation_count: 1,
|
|
20
|
+
impact_score: 0.1, health_score: 0.1, error_rate: 0.8, avg_latency_ms: 3000 }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
let(:all_exts) { [cognition_ext, memory_ext, low_ext] }
|
|
24
|
+
|
|
25
|
+
# ─── extension_timeline ────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe '.extension_timeline' do
|
|
28
|
+
it 'returns success: true' do
|
|
29
|
+
result = dashboard.extension_timeline(extensions: all_exts)
|
|
30
|
+
expect(result[:success]).to be true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'returns a series array' do
|
|
34
|
+
result = dashboard.extension_timeline(extensions: all_exts)
|
|
35
|
+
expect(result[:series]).to be_an(Array)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns at least one series point' do
|
|
39
|
+
result = dashboard.extension_timeline(extensions: all_exts)
|
|
40
|
+
expect(result[:series].size).to be >= 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'returns range_days matching requested days' do
|
|
44
|
+
result = dashboard.extension_timeline(extensions: all_exts, days: 7)
|
|
45
|
+
expect(result[:range_days]).to eq(7)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'series points include date and count keys' do
|
|
49
|
+
result = dashboard.extension_timeline(extensions: all_exts)
|
|
50
|
+
result[:series].each do |point|
|
|
51
|
+
expect(point).to have_key(:date)
|
|
52
|
+
expect(point).to have_key(:count)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'reflects current extension count in the series' do
|
|
57
|
+
result = dashboard.extension_timeline(extensions: all_exts)
|
|
58
|
+
expect(result[:series].last[:count]).to eq(3)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns count 0 for empty extensions' do
|
|
62
|
+
result = dashboard.extension_timeline(extensions: [])
|
|
63
|
+
expect(result[:series].last[:count]).to eq(0)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ─── category_distribution ─────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe '.category_distribution' do
|
|
70
|
+
it 'returns success: true' do
|
|
71
|
+
result = dashboard.category_distribution(extensions: all_exts)
|
|
72
|
+
expect(result[:success]).to be true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns a distribution hash' do
|
|
76
|
+
result = dashboard.category_distribution(extensions: all_exts)
|
|
77
|
+
expect(result[:distribution]).to be_a(Hash)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns total equal to extension count' do
|
|
81
|
+
result = dashboard.category_distribution(extensions: all_exts)
|
|
82
|
+
expect(result[:total]).to eq(3)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'includes all CATEGORIES keys in distribution' do
|
|
86
|
+
result = dashboard.category_distribution(extensions: all_exts)
|
|
87
|
+
Legion::Extensions::MindGrowth::Helpers::Constants::CATEGORIES.each do |cat|
|
|
88
|
+
expect(result[:distribution]).to have_key(cat)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'counts extensions per category correctly' do
|
|
93
|
+
result = dashboard.category_distribution(extensions: all_exts)
|
|
94
|
+
expect(result[:distribution][:cognition]).to eq(1)
|
|
95
|
+
expect(result[:distribution][:memory]).to eq(1)
|
|
96
|
+
expect(result[:distribution][:perception]).to eq(1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'returns zero counts for unpopulated categories' do
|
|
100
|
+
result = dashboard.category_distribution(extensions: [cognition_ext])
|
|
101
|
+
expect(result[:distribution][:memory]).to eq(0)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'defaults missing category to :cognition' do
|
|
105
|
+
ext = { name: 'lex-bare' }
|
|
106
|
+
result = dashboard.category_distribution(extensions: [ext])
|
|
107
|
+
expect(result[:distribution][:cognition]).to eq(1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'returns total: 0 for empty extensions' do
|
|
111
|
+
result = dashboard.category_distribution(extensions: [])
|
|
112
|
+
expect(result[:total]).to eq(0)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ─── build_metrics ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe '.build_metrics' do
|
|
119
|
+
it 'returns success: true' do
|
|
120
|
+
result = dashboard.build_metrics
|
|
121
|
+
expect(result[:success]).to be true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'returns total_proposals' do
|
|
125
|
+
result = dashboard.build_metrics
|
|
126
|
+
expect(result).to have_key(:total_proposals)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'returns approved count' do
|
|
130
|
+
result = dashboard.build_metrics
|
|
131
|
+
expect(result).to have_key(:approved)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns rejected count' do
|
|
135
|
+
result = dashboard.build_metrics
|
|
136
|
+
expect(result).to have_key(:rejected)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns built count' do
|
|
140
|
+
result = dashboard.build_metrics
|
|
141
|
+
expect(result).to have_key(:built)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'returns failed count' do
|
|
145
|
+
result = dashboard.build_metrics
|
|
146
|
+
expect(result).to have_key(:failed)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'returns success_rate as a numeric' do
|
|
150
|
+
result = dashboard.build_metrics
|
|
151
|
+
expect(result[:success_rate]).to be_a(Numeric)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'returns approval_rate as a numeric' do
|
|
155
|
+
result = dashboard.build_metrics
|
|
156
|
+
expect(result[:approval_rate]).to be_a(Numeric)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'returns success_rate 0.0 when no builds attempted' do
|
|
160
|
+
result = dashboard.build_metrics
|
|
161
|
+
expect(result[:success_rate]).to eq(0.0)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'returns approval_rate 0.0 when no proposals evaluated' do
|
|
165
|
+
result = dashboard.build_metrics
|
|
166
|
+
expect(result[:approval_rate]).to eq(0.0)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ─── top_extensions ────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe '.top_extensions' do
|
|
173
|
+
it 'returns success: true' do
|
|
174
|
+
result = dashboard.top_extensions(extensions: all_exts)
|
|
175
|
+
expect(result[:success]).to be true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'returns a top array' do
|
|
179
|
+
result = dashboard.top_extensions(extensions: all_exts)
|
|
180
|
+
expect(result[:top]).to be_an(Array)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'returns limit in response' do
|
|
184
|
+
result = dashboard.top_extensions(extensions: all_exts, limit: 2)
|
|
185
|
+
expect(result[:limit]).to eq(2)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'respects the limit parameter' do
|
|
189
|
+
result = dashboard.top_extensions(extensions: all_exts, limit: 2)
|
|
190
|
+
expect(result[:top].size).to be <= 2
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'returns highest-fitness extension first' do
|
|
194
|
+
result = dashboard.top_extensions(extensions: all_exts, limit: 1)
|
|
195
|
+
expect(result[:top].first[:name]).to eq('lex-cognition')
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'each entry includes name, invocation_count, and fitness' do
|
|
199
|
+
result = dashboard.top_extensions(extensions: all_exts)
|
|
200
|
+
result[:top].each do |entry|
|
|
201
|
+
expect(entry).to have_key(:name)
|
|
202
|
+
expect(entry).to have_key(:invocation_count)
|
|
203
|
+
expect(entry).to have_key(:fitness)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'returns empty top array for empty extensions' do
|
|
208
|
+
result = dashboard.top_extensions(extensions: [])
|
|
209
|
+
expect(result[:top]).to be_empty
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ─── bottom_extensions ─────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
describe '.bottom_extensions' do
|
|
216
|
+
it 'returns success: true' do
|
|
217
|
+
result = dashboard.bottom_extensions(extensions: all_exts)
|
|
218
|
+
expect(result[:success]).to be true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'returns a bottom array' do
|
|
222
|
+
result = dashboard.bottom_extensions(extensions: all_exts)
|
|
223
|
+
expect(result[:bottom]).to be_an(Array)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'returns limit in response' do
|
|
227
|
+
result = dashboard.bottom_extensions(extensions: all_exts, limit: 2)
|
|
228
|
+
expect(result[:limit]).to eq(2)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'respects the limit parameter' do
|
|
232
|
+
result = dashboard.bottom_extensions(extensions: all_exts, limit: 2)
|
|
233
|
+
expect(result[:bottom].size).to be <= 2
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'returns lowest-fitness extension first' do
|
|
237
|
+
result = dashboard.bottom_extensions(extensions: all_exts, limit: 1)
|
|
238
|
+
expect(result[:bottom].first[:name]).to eq('lex-low')
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'each entry includes name, invocation_count, and fitness' do
|
|
242
|
+
result = dashboard.bottom_extensions(extensions: all_exts)
|
|
243
|
+
result[:bottom].each do |entry|
|
|
244
|
+
expect(entry).to have_key(:name)
|
|
245
|
+
expect(entry).to have_key(:invocation_count)
|
|
246
|
+
expect(entry).to have_key(:fitness)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'returns empty bottom array for empty extensions' do
|
|
251
|
+
result = dashboard.bottom_extensions(extensions: [])
|
|
252
|
+
expect(result[:bottom]).to be_empty
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ─── recent_proposals ──────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
describe '.recent_proposals' do
|
|
259
|
+
it 'returns success: true' do
|
|
260
|
+
result = dashboard.recent_proposals
|
|
261
|
+
expect(result[:success]).to be true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'returns a proposals array' do
|
|
265
|
+
result = dashboard.recent_proposals
|
|
266
|
+
expect(result[:proposals]).to be_an(Array)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'returns count matching proposals array size' do
|
|
270
|
+
result = dashboard.recent_proposals
|
|
271
|
+
expect(result[:count]).to eq(result[:proposals].size)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'delegates to Proposer.list_proposals' do
|
|
275
|
+
allow(Legion::Extensions::MindGrowth::Runners::Proposer)
|
|
276
|
+
.to receive(:list_proposals).with(limit: 5).and_call_original
|
|
277
|
+
dashboard.recent_proposals(limit: 5)
|
|
278
|
+
expect(Legion::Extensions::MindGrowth::Runners::Proposer)
|
|
279
|
+
.to have_received(:list_proposals).with(limit: 5)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'returns proposals when store has entries' do
|
|
283
|
+
Legion::Extensions::MindGrowth::Runners::Proposer.propose_concept(
|
|
284
|
+
name: 'lex-dash-test', category: :cognition, description: 'dashboard test', enrich: false
|
|
285
|
+
)
|
|
286
|
+
result = dashboard.recent_proposals(limit: 10)
|
|
287
|
+
expect(result[:count]).to be >= 1
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it 'returns empty list when no proposals exist' do
|
|
291
|
+
result = dashboard.recent_proposals
|
|
292
|
+
expect(result[:proposals]).to be_an(Array)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# ─── full_dashboard ────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
describe '.full_dashboard' do
|
|
299
|
+
it 'returns success: true' do
|
|
300
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
301
|
+
expect(result[:success]).to be true
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it 'includes category_distribution' do
|
|
305
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
306
|
+
expect(result[:category_distribution]).to be_a(Hash)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it 'includes build_metrics' do
|
|
310
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
311
|
+
expect(result[:build_metrics]).to be_a(Hash)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'includes top_extensions array' do
|
|
315
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
316
|
+
expect(result[:top_extensions]).to be_an(Array)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'includes bottom_extensions array' do
|
|
320
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
321
|
+
expect(result[:bottom_extensions]).to be_an(Array)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'includes recent_proposals array' do
|
|
325
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
326
|
+
expect(result[:recent_proposals]).to be_an(Array)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'includes health_summary hash' do
|
|
330
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
331
|
+
expect(result[:health_summary]).to be_a(Hash)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it 'includes a timestamp string' do
|
|
335
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
336
|
+
expect(result[:timestamp]).to be_a(String)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it 'health_summary reflects extension count' do
|
|
340
|
+
result = dashboard.full_dashboard(extensions: all_exts)
|
|
341
|
+
expect(result[:health_summary][:total]).to eq(3)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it 'returns empty arrays and zero counts for empty extensions' do
|
|
345
|
+
result = dashboard.full_dashboard(extensions: [])
|
|
346
|
+
expect(result[:top_extensions]).to be_empty
|
|
347
|
+
expect(result[:bottom_extensions]).to be_empty
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MindGrowth::Runners::Evolver do
|
|
4
|
+
subject(:evolver) { described_class }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
Legion::Extensions::MindGrowth::Runners::Proposer.instance_variable_set(:@proposal_store, nil)
|
|
8
|
+
evolver.instance_variable_set(:@status_store, nil)
|
|
9
|
+
evolver.instance_variable_set(:@replacement_map, nil)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:high_ext) do
|
|
13
|
+
{ name: 'lex-high', invocation_count: 5000, impact_score: 0.9,
|
|
14
|
+
health_score: 1.0, error_rate: 0.0, avg_latency_ms: 0, status: :active }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:mid_ext) do
|
|
18
|
+
{ name: 'lex-mid', invocation_count: 50, impact_score: 0.5,
|
|
19
|
+
health_score: 0.7, error_rate: 0.1, avg_latency_ms: 200, status: :active }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:low_ext) do
|
|
23
|
+
{ name: 'lex-low', invocation_count: 1, impact_score: 0.1,
|
|
24
|
+
health_score: 0.1, error_rate: 0.5, avg_latency_ms: 2000, status: :active }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:building_ext) do
|
|
28
|
+
{ name: 'lex-building', invocation_count: 0, impact_score: 0.0,
|
|
29
|
+
health_score: 0.0, error_rate: 1.0, avg_latency_ms: 5000, status: :building }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:testing_ext) do
|
|
33
|
+
{ name: 'lex-testing', invocation_count: 0, impact_score: 0.0,
|
|
34
|
+
health_score: 0.0, error_rate: 1.0, avg_latency_ms: 5000, status: :testing }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ─── select_for_improvement ────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe '.select_for_improvement' do
|
|
40
|
+
it 'returns success: true' do
|
|
41
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext])
|
|
42
|
+
expect(result[:success]).to be true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns a candidates array' do
|
|
46
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext])
|
|
47
|
+
expect(result[:candidates]).to be_an(Array)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns count matching candidates array size' do
|
|
51
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext])
|
|
52
|
+
expect(result[:count]).to eq(result[:candidates].size)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'returns total_evaluated count' do
|
|
56
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext])
|
|
57
|
+
expect(result[:total_evaluated]).to eq(3)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'selects the bottom N extensions by fitness' do
|
|
61
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext], count: 1)
|
|
62
|
+
expect(result[:candidates].first[:name]).to eq('lex-low')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'respects the count parameter' do
|
|
66
|
+
result = evolver.select_for_improvement(extensions: [high_ext, mid_ext, low_ext], count: 2)
|
|
67
|
+
expect(result[:count]).to eq(2)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'skips extensions with :building status' do
|
|
71
|
+
result = evolver.select_for_improvement(extensions: [high_ext, building_ext], count: 2)
|
|
72
|
+
names = result[:candidates].map { |c| c[:name] }
|
|
73
|
+
expect(names).not_to include('lex-building')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'skips extensions with :testing status' do
|
|
77
|
+
result = evolver.select_for_improvement(extensions: [high_ext, testing_ext], count: 2)
|
|
78
|
+
names = result[:candidates].map { |c| c[:name] }
|
|
79
|
+
expect(names).not_to include('lex-testing')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'does not count skipped extensions in total_evaluated' do
|
|
83
|
+
result = evolver.select_for_improvement(extensions: [high_ext, building_ext, low_ext], count: 2)
|
|
84
|
+
expect(result[:total_evaluated]).to eq(2)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns empty candidates for empty extensions list' do
|
|
88
|
+
result = evolver.select_for_improvement(extensions: [])
|
|
89
|
+
expect(result[:candidates]).to be_empty
|
|
90
|
+
expect(result[:count]).to eq(0)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'handles count greater than total extensions' do
|
|
94
|
+
result = evolver.select_for_improvement(extensions: [high_ext], count: 10)
|
|
95
|
+
expect(result[:count]).to eq(1)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns all extensions when count equals list size' do
|
|
99
|
+
result = evolver.select_for_improvement(extensions: [high_ext, low_ext], count: 2)
|
|
100
|
+
expect(result[:count]).to eq(2)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ─── propose_improvement ───────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe '.propose_improvement' do
|
|
107
|
+
it 'returns success: true' do
|
|
108
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
109
|
+
expect(result[:success]).to be true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns the extension_name' do
|
|
113
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
114
|
+
expect(result[:extension_name]).to eq('lex-low')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns a numeric fitness' do
|
|
118
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
119
|
+
expect(result[:fitness]).to be_a(Numeric)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'returns a weaknesses array' do
|
|
123
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
124
|
+
expect(result[:weaknesses]).to be_an(Array)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns a suggestions array' do
|
|
128
|
+
begin
|
|
129
|
+
evolver.propose_improve(extension: low_ext)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
134
|
+
expect(result[:suggestions]).to be_an(Array)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'identifies :low_invocations weakness for zero-invocation extension' do
|
|
138
|
+
ext = { name: 'lex-zero', invocation_count: 0, impact_score: 0.5, error_rate: 0.0, avg_latency_ms: 0 }
|
|
139
|
+
result = evolver.propose_improvement(extension: ext)
|
|
140
|
+
expect(result[:weaknesses]).to include(:low_invocations)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'identifies :high_error_rate weakness when error_rate > 0.2' do
|
|
144
|
+
ext = { name: 'lex-err', invocation_count: 100, impact_score: 0.5, error_rate: 0.5, avg_latency_ms: 0 }
|
|
145
|
+
result = evolver.propose_improvement(extension: ext)
|
|
146
|
+
expect(result[:weaknesses]).to include(:high_error_rate)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'identifies :high_latency weakness when avg_latency_ms > 1000' do
|
|
150
|
+
ext = { name: 'lex-slow', invocation_count: 100, impact_score: 0.5, error_rate: 0.0, avg_latency_ms: 2000 }
|
|
151
|
+
result = evolver.propose_improvement(extension: ext)
|
|
152
|
+
expect(result[:weaknesses]).to include(:high_latency)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'identifies :low_impact weakness when impact_score < 0.3' do
|
|
156
|
+
ext = { name: 'lex-lowin', invocation_count: 100, impact_score: 0.1, error_rate: 0.0, avg_latency_ms: 0 }
|
|
157
|
+
result = evolver.propose_improvement(extension: ext)
|
|
158
|
+
expect(result[:weaknesses]).to include(:low_impact)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'returns generic suggestion when no specific weaknesses detected' do
|
|
162
|
+
result = evolver.propose_improvement(extension: high_ext)
|
|
163
|
+
expect(result[:suggestions]).not_to be_empty
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if defined?(Legion::LLM)
|
|
167
|
+
it 'returns LLM-enriched suggestions when LLM is available' do
|
|
168
|
+
mock_llm_session = double('session')
|
|
169
|
+
mock_response = double('response', content: '["fix error handling", "add caching"]')
|
|
170
|
+
allow(mock_llm_session).to receive(:ask).and_return(mock_response)
|
|
171
|
+
allow(Legion::LLM).to receive(:started?).and_return(true)
|
|
172
|
+
allow(Legion::LLM).to receive(:chat).and_return(mock_llm_session)
|
|
173
|
+
|
|
174
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
175
|
+
expect(result[:suggestions]).to include('fix error handling')
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'falls back to heuristic suggestions when LLM is unavailable' do
|
|
180
|
+
begin
|
|
181
|
+
allow(Legion).to receive(:const_defined?).with(:LLM).and_return(false)
|
|
182
|
+
rescue StandardError
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
186
|
+
expect(result[:suggestions]).to be_an(Array)
|
|
187
|
+
expect(result[:suggestions]).not_to be_empty
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# ─── replace_extension ─────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe '.replace_extension' do
|
|
194
|
+
it 'returns success: true' do
|
|
195
|
+
result = evolver.replace_extension(old_name: 'lex-old', new_proposal_id: 'abc-123')
|
|
196
|
+
expect(result[:success]).to be true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'returns the replaced name' do
|
|
200
|
+
result = evolver.replace_extension(old_name: 'lex-old', new_proposal_id: 'abc-123')
|
|
201
|
+
expect(result[:replaced]).to eq('lex-old')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'returns the replacement_proposal_id' do
|
|
205
|
+
result = evolver.replace_extension(old_name: 'lex-old', new_proposal_id: 'abc-123')
|
|
206
|
+
expect(result[:replacement_proposal_id]).to eq('abc-123')
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'marks the old extension as :pruned in the status store' do
|
|
210
|
+
evolver.replace_extension(old_name: 'lex-prunable', new_proposal_id: 'xyz-999')
|
|
211
|
+
expect(evolver.instance_variable_get(:@status_store)['lex-prunable']).to eq(:pruned)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it 'stores the replacement mapping' do
|
|
215
|
+
evolver.replace_extension(old_name: 'lex-old2', new_proposal_id: 'new-id')
|
|
216
|
+
expect(evolver.instance_variable_get(:@replacement_map)['lex-old2']).to eq('new-id')
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ─── merge_extensions ──────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
describe '.merge_extensions' do
|
|
223
|
+
let(:ext_a) { { name: 'lex-alpha', category: :cognition } }
|
|
224
|
+
let(:ext_b) { { name: 'lex-beta', category: :memory } }
|
|
225
|
+
|
|
226
|
+
it 'returns success: true' do
|
|
227
|
+
result = evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b)
|
|
228
|
+
expect(result[:success]).to be true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'returns a merged_proposal' do
|
|
232
|
+
result = evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b)
|
|
233
|
+
expect(result[:merged_proposal]).to be_a(Hash)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'returns sources with both extension names' do
|
|
237
|
+
result = evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b)
|
|
238
|
+
expect(result[:sources]).to contain_exactly('lex-alpha', 'lex-beta')
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'calls Proposer.propose_concept to create the merged proposal' do
|
|
242
|
+
allow(Legion::Extensions::MindGrowth::Runners::Proposer)
|
|
243
|
+
.to receive(:propose_concept).and_call_original
|
|
244
|
+
evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b)
|
|
245
|
+
expect(Legion::Extensions::MindGrowth::Runners::Proposer)
|
|
246
|
+
.to have_received(:propose_concept)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'uses provided merged_name when given' do
|
|
250
|
+
result = evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b, merged_name: 'lex-custom')
|
|
251
|
+
proposal = result[:merged_proposal]
|
|
252
|
+
expect(proposal[:success]).to be true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'auto-generates merged name from source names when none provided' do
|
|
256
|
+
allow(Legion::Extensions::MindGrowth::Runners::Proposer)
|
|
257
|
+
.to receive(:propose_concept) do |**args|
|
|
258
|
+
expect(args[:name]).to include('alpha')
|
|
259
|
+
{ success: true, proposal: { name: args[:name] } }
|
|
260
|
+
end
|
|
261
|
+
evolver.merge_extensions(extension_a: ext_a, extension_b: ext_b)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ─── evolution_summary ─────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe '.evolution_summary' do
|
|
268
|
+
let(:all_exts) { [high_ext, mid_ext, low_ext] }
|
|
269
|
+
|
|
270
|
+
it 'returns success: true' do
|
|
271
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
272
|
+
expect(result[:success]).to be true
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'returns improvement_candidates array' do
|
|
276
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
277
|
+
expect(result[:improvement_candidates]).to be_an(Array)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'returns prune_candidates array' do
|
|
281
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
282
|
+
expect(result[:prune_candidates]).to be_an(Array)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'returns speciation_candidates array' do
|
|
286
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
287
|
+
expect(result[:speciation_candidates]).to be_an(Array)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it 'returns fitness_distribution hash with min/max/mean/median' do
|
|
291
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
292
|
+
dist = result[:fitness_distribution]
|
|
293
|
+
expect(dist).to have_key(:min)
|
|
294
|
+
expect(dist).to have_key(:max)
|
|
295
|
+
expect(dist).to have_key(:mean)
|
|
296
|
+
expect(dist).to have_key(:median)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'fitness_distribution min <= mean <= max' do
|
|
300
|
+
result = evolver.evolution_summary(extensions: all_exts)
|
|
301
|
+
dist = result[:fitness_distribution]
|
|
302
|
+
expect(dist[:min]).to be <= dist[:mean]
|
|
303
|
+
expect(dist[:mean]).to be <= dist[:max]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'includes low-fitness extension in improvement_candidates' do
|
|
307
|
+
result = evolver.evolution_summary(extensions: [high_ext, low_ext], count: 1)
|
|
308
|
+
names = result[:improvement_candidates].map { |c| c[:name] }
|
|
309
|
+
expect(names).to include('lex-low')
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'identifies speciation candidates by drift_score' do
|
|
313
|
+
drifted = { name: 'lex-drifted', drift_score: 0.8, invocation_count: 10,
|
|
314
|
+
impact_score: 0.5, health_score: 0.5, error_rate: 0.0, avg_latency_ms: 0 }
|
|
315
|
+
result = evolver.evolution_summary(extensions: [drifted])
|
|
316
|
+
expect(result[:speciation_candidates]).to include('lex-drifted')
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'does not flag extension with drift_score below threshold as speciation candidate' do
|
|
320
|
+
stable = { name: 'lex-stable', drift_score: 0.1, invocation_count: 100,
|
|
321
|
+
impact_score: 0.7, health_score: 0.9, error_rate: 0.0, avg_latency_ms: 0 }
|
|
322
|
+
result = evolver.evolution_summary(extensions: [stable])
|
|
323
|
+
expect(result[:speciation_candidates]).to be_empty
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'returns all-zero distribution for empty extensions' do
|
|
327
|
+
result = evolver.evolution_summary(extensions: [])
|
|
328
|
+
dist = result[:fitness_distribution]
|
|
329
|
+
expect(dist[:min]).to eq(0.0)
|
|
330
|
+
expect(dist[:max]).to eq(0.0)
|
|
331
|
+
expect(dist[:mean]).to eq(0.0)
|
|
332
|
+
expect(dist[:median]).to eq(0.0)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'handles all extensions having the same fitness' do
|
|
336
|
+
same = Array.new(3) do |i|
|
|
337
|
+
{ name: "lex-same-#{i}", invocation_count: 0, impact_score: 0.0,
|
|
338
|
+
health_score: 0.0, error_rate: 0.0, avg_latency_ms: 0 }
|
|
339
|
+
end
|
|
340
|
+
result = evolver.evolution_summary(extensions: same)
|
|
341
|
+
dist = result[:fitness_distribution]
|
|
342
|
+
expect(dist[:min]).to eq(dist[:max])
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# ─── constants ─────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
describe 'constants' do
|
|
349
|
+
it 'defines BOTTOM_PERCENTILE as 0.05' do
|
|
350
|
+
expect(described_class::BOTTOM_PERCENTILE).to eq(0.05)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it 'defines SPECIATION_DRIFT_THRESHOLD as 0.5' do
|
|
354
|
+
expect(described_class::SPECIATION_DRIFT_THRESHOLD).to eq(0.5)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-mind-growth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -145,7 +145,9 @@ files:
|
|
|
145
145
|
- lib/legion/extensions/mind_growth/runners/analyzer.rb
|
|
146
146
|
- lib/legion/extensions/mind_growth/runners/builder.rb
|
|
147
147
|
- lib/legion/extensions/mind_growth/runners/composer.rb
|
|
148
|
+
- lib/legion/extensions/mind_growth/runners/dashboard.rb
|
|
148
149
|
- lib/legion/extensions/mind_growth/runners/dream_ideation.rb
|
|
150
|
+
- lib/legion/extensions/mind_growth/runners/evolver.rb
|
|
149
151
|
- lib/legion/extensions/mind_growth/runners/governance.rb
|
|
150
152
|
- lib/legion/extensions/mind_growth/runners/integration_tester.rb
|
|
151
153
|
- lib/legion/extensions/mind_growth/runners/monitor.rb
|
|
@@ -168,7 +170,9 @@ files:
|
|
|
168
170
|
- spec/legion/extensions/mind_growth/runners/analyzer_spec.rb
|
|
169
171
|
- spec/legion/extensions/mind_growth/runners/builder_spec.rb
|
|
170
172
|
- spec/legion/extensions/mind_growth/runners/composer_spec.rb
|
|
173
|
+
- spec/legion/extensions/mind_growth/runners/dashboard_spec.rb
|
|
171
174
|
- spec/legion/extensions/mind_growth/runners/dream_ideation_spec.rb
|
|
175
|
+
- spec/legion/extensions/mind_growth/runners/evolver_spec.rb
|
|
172
176
|
- spec/legion/extensions/mind_growth/runners/governance_spec.rb
|
|
173
177
|
- spec/legion/extensions/mind_growth/runners/integration_tester_spec.rb
|
|
174
178
|
- spec/legion/extensions/mind_growth/runners/monitor_spec.rb
|