lex-mind-growth 0.2.0 → 0.2.1
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 +9 -0
- data/lib/legion/extensions/mind_growth/runners/competitive_evolver.rb +165 -0
- data/lib/legion/extensions/mind_growth/version.rb +1 -1
- data/lib/legion/extensions/mind_growth.rb +1 -0
- data/spec/legion/extensions/mind_growth/runners/competitive_evolver_spec.rb +340 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44ba3c4f6e43488f8e92edb6be0b6204890b58f76220efd964c924db011674b3
|
|
4
|
+
data.tar.gz: 571392567974177a54cc4d3df6b094ad895849f3de415b271f0133bb3ed6d40d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23e9288fa07cce5693c0aa7f9475ad7f6f9c2f7e211fecf39c4dc4f2309ed674115537358dbcf65dda6b3091e1153927716b2d17798b16ff7b368d7576f6612c
|
|
7
|
+
data.tar.gz: 7a141886553785dca8178cacb63f5021be1ae3aa1eee80003f86e06b5b3a641778e5cc85f282db229916d4ab0d812a4c0c0d82b3d8e652061578043e51a961d2
|
|
@@ -91,6 +91,15 @@ module Legion
|
|
|
91
91
|
def resolve_disagreement(**) = Runners::ConsensusBuilder.resolve_disagreement(**)
|
|
92
92
|
def consensus_summary(**) = Runners::ConsensusBuilder.consensus_summary(**)
|
|
93
93
|
|
|
94
|
+
# CompetitiveEvolver delegation
|
|
95
|
+
def create_competition(**) = Runners::CompetitiveEvolver.create_competition(**)
|
|
96
|
+
def run_trial(**) = Runners::CompetitiveEvolver.run_trial(**)
|
|
97
|
+
def compare_results(**) = Runners::CompetitiveEvolver.compare_results(**)
|
|
98
|
+
def declare_winner(**) = Runners::CompetitiveEvolver.declare_winner(**)
|
|
99
|
+
def competition_status(**) = Runners::CompetitiveEvolver.competition_status(**)
|
|
100
|
+
def active_competitions(**) = Runners::CompetitiveEvolver.active_competitions(**)
|
|
101
|
+
def competition_history(**) = Runners::CompetitiveEvolver.competition_history(**)
|
|
102
|
+
|
|
94
103
|
# Dashboard delegation
|
|
95
104
|
def extension_timeline(**) = Runners::Dashboard.extension_timeline(**)
|
|
96
105
|
def category_distribution(**) = Runners::Dashboard.category_distribution(**)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module CompetitiveEvolver
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
COMPETITION_STATUSES = %i[pending active evaluating decided cancelled].freeze
|
|
11
|
+
MIN_TRIAL_ITERATIONS = 10
|
|
12
|
+
|
|
13
|
+
def create_competition(gap:, proposal_ids:, **)
|
|
14
|
+
ids = Array(proposal_ids)
|
|
15
|
+
return { success: false, reason: :insufficient_competitors } if ids.size < 2
|
|
16
|
+
|
|
17
|
+
competition_id = SecureRandom.uuid
|
|
18
|
+
competition = {
|
|
19
|
+
id: competition_id,
|
|
20
|
+
gap: gap.to_s,
|
|
21
|
+
proposal_ids: ids,
|
|
22
|
+
status: :pending,
|
|
23
|
+
trials: {},
|
|
24
|
+
winner: nil,
|
|
25
|
+
created_at: Time.now.utc,
|
|
26
|
+
decided_at: nil
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
store_competition(competition)
|
|
30
|
+
{ success: true, competition_id: competition_id, gap: gap.to_s, competitors: ids.size }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run_trial(competition_id:, extension:, iterations: MIN_TRIAL_ITERATIONS, **)
|
|
34
|
+
competition = get_competition(competition_id)
|
|
35
|
+
return { success: false, reason: :not_found } unless competition
|
|
36
|
+
return { success: false, reason: :already_decided } if competition[:status] == :decided
|
|
37
|
+
|
|
38
|
+
transition_competition(competition_id, :active) if competition[:status] == :pending
|
|
39
|
+
|
|
40
|
+
fitness = Helpers::FitnessEvaluator.fitness(extension)
|
|
41
|
+
name = extension[:name] || extension[:extension_name]
|
|
42
|
+
|
|
43
|
+
trial = {
|
|
44
|
+
extension_name: name,
|
|
45
|
+
fitness: fitness,
|
|
46
|
+
error_rate: extension[:error_rate] || 0.0,
|
|
47
|
+
avg_latency_ms: extension[:avg_latency_ms] || 0,
|
|
48
|
+
invocations: extension[:invocation_count] || 0,
|
|
49
|
+
iterations: iterations,
|
|
50
|
+
recorded_at: Time.now.utc
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
record_trial(competition_id, name, trial)
|
|
54
|
+
{ success: true, competition_id: competition_id, trial: trial }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def compare_results(competition_id:, **)
|
|
58
|
+
competition = get_competition(competition_id)
|
|
59
|
+
return { success: false, reason: :not_found } unless competition
|
|
60
|
+
|
|
61
|
+
trials = competition[:trials]
|
|
62
|
+
return { success: true, comparison: [], leader: nil } if trials.empty?
|
|
63
|
+
|
|
64
|
+
ranked = trials.values.sort_by { |t| [-t[:fitness], t[:avg_latency_ms]] }
|
|
65
|
+
leader = ranked.first
|
|
66
|
+
|
|
67
|
+
comparison = ranked.map.with_index(1) do |trial, rank|
|
|
68
|
+
{
|
|
69
|
+
extension_name: trial[:extension_name],
|
|
70
|
+
fitness: trial[:fitness],
|
|
71
|
+
error_rate: trial[:error_rate],
|
|
72
|
+
avg_latency_ms: trial[:avg_latency_ms],
|
|
73
|
+
rank: rank
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{ success: true, comparison: comparison, leader: leader[:extension_name] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def declare_winner(competition_id:, **)
|
|
81
|
+
competition = get_competition(competition_id)
|
|
82
|
+
return { success: false, reason: :not_found } unless competition
|
|
83
|
+
return { success: false, reason: :already_decided } if competition[:status] == :decided
|
|
84
|
+
return { success: false, reason: :no_trials } if competition[:trials].empty?
|
|
85
|
+
|
|
86
|
+
comparison = compare_results(competition_id: competition_id)
|
|
87
|
+
winner_name = comparison[:leader]
|
|
88
|
+
|
|
89
|
+
losers = competition[:trials].keys.reject { |name| name == winner_name }
|
|
90
|
+
|
|
91
|
+
losers.each do |loser_name|
|
|
92
|
+
Runners::Evolver.replace_extension(old_name: loser_name, new_proposal_id: "winner:#{winner_name}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
transition_competition(competition_id, :decided)
|
|
96
|
+
set_winner(competition_id, winner_name)
|
|
97
|
+
|
|
98
|
+
{ success: true, winner: winner_name, losers: losers, competition_id: competition_id }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def competition_status(competition_id:, **)
|
|
102
|
+
competition = get_competition(competition_id)
|
|
103
|
+
return { success: false, reason: :not_found } unless competition
|
|
104
|
+
|
|
105
|
+
{ success: true, id: competition[:id], gap: competition[:gap],
|
|
106
|
+
status: competition[:status], competitors: competition[:proposal_ids],
|
|
107
|
+
trial_count: competition[:trials].size, winner: competition[:winner] }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def active_competitions(**)
|
|
111
|
+
comps = all_competitions.select { |c| %i[pending active evaluating].include?(c[:status]) }
|
|
112
|
+
{ success: true, competitions: comps.map { |c| { id: c[:id], gap: c[:gap], status: c[:status] } },
|
|
113
|
+
count: comps.size }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def competition_history(limit: 20, **)
|
|
117
|
+
comps = all_competitions.sort_by { |c| c[:created_at] }.reverse.first(limit)
|
|
118
|
+
entries = comps.map do |c|
|
|
119
|
+
{ id: c[:id], gap: c[:gap], status: c[:status], winner: c[:winner],
|
|
120
|
+
competitors: c[:proposal_ids].size, trial_count: c[:trials].size }
|
|
121
|
+
end
|
|
122
|
+
{ success: true, competitions: entries, count: entries.size }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def competitions
|
|
128
|
+
@competitions ||= {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def mutex
|
|
132
|
+
@mutex ||= Mutex.new
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def store_competition(competition)
|
|
136
|
+
mutex.synchronize { competitions[competition[:id]] = competition }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def get_competition(id)
|
|
140
|
+
mutex.synchronize { competitions[id]&.dup }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def all_competitions
|
|
144
|
+
mutex.synchronize { competitions.values.map(&:dup) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def transition_competition(id, new_status)
|
|
148
|
+
mutex.synchronize do
|
|
149
|
+
competitions[id][:status] = new_status
|
|
150
|
+
competitions[id][:decided_at] = Time.now.utc if new_status == :decided
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def set_winner(id, winner_name)
|
|
155
|
+
mutex.synchronize { competitions[id][:winner] = winner_name }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def record_trial(competition_id, name, trial)
|
|
159
|
+
mutex.synchronize { competitions[competition_id][:trials][name] = trial }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -28,6 +28,7 @@ require 'legion/extensions/mind_growth/runners/evolver'
|
|
|
28
28
|
require 'legion/extensions/mind_growth/runners/dashboard'
|
|
29
29
|
require 'legion/extensions/mind_growth/runners/swarm_builder'
|
|
30
30
|
require 'legion/extensions/mind_growth/runners/consensus_builder'
|
|
31
|
+
require 'legion/extensions/mind_growth/runners/competitive_evolver'
|
|
31
32
|
require 'legion/extensions/mind_growth/client'
|
|
32
33
|
|
|
33
34
|
module Legion
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MindGrowth::Runners::CompetitiveEvolver do
|
|
4
|
+
subject(:evolver) { described_class }
|
|
5
|
+
|
|
6
|
+
# Reset internal state between tests
|
|
7
|
+
after do
|
|
8
|
+
described_class.instance_variable_set(:@competitions, {})
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# ─── helpers ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
def build_extension(name:, invocation_count: 100, error_rate: 0.01, avg_latency_ms: 50,
|
|
14
|
+
impact_score: 0.7, health_score: 0.8, category: :cognition)
|
|
15
|
+
{ name: name, invocation_count: invocation_count, error_rate: error_rate,
|
|
16
|
+
avg_latency_ms: avg_latency_ms, impact_score: impact_score,
|
|
17
|
+
health_score: health_score, category: category }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_and_return_id(gap: 'working_memory', proposal_ids: %w[p1 p2])
|
|
21
|
+
result = evolver.create_competition(gap: gap, proposal_ids: proposal_ids)
|
|
22
|
+
result[:competition_id]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ─── constants ───────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe 'constants' do
|
|
28
|
+
it 'defines COMPETITION_STATUSES' do
|
|
29
|
+
expect(described_class::COMPETITION_STATUSES).to contain_exactly(
|
|
30
|
+
:pending, :active, :evaluating, :decided, :cancelled
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'defines MIN_TRIAL_ITERATIONS as 10' do
|
|
35
|
+
expect(described_class::MIN_TRIAL_ITERATIONS).to eq(10)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ─── create_competition ─────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe '.create_competition' do
|
|
42
|
+
it 'returns success: true with a competition_id' do
|
|
43
|
+
result = evolver.create_competition(gap: 'attention', proposal_ids: %w[p1 p2])
|
|
44
|
+
expect(result[:success]).to be true
|
|
45
|
+
expect(result[:competition_id]).to be_a(String)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns the gap and competitor count' do
|
|
49
|
+
result = evolver.create_competition(gap: 'attention', proposal_ids: %w[p1 p2 p3])
|
|
50
|
+
expect(result[:gap]).to eq('attention')
|
|
51
|
+
expect(result[:competitors]).to eq(3)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'requires at least 2 competitors' do
|
|
55
|
+
result = evolver.create_competition(gap: 'attention', proposal_ids: ['p1'])
|
|
56
|
+
expect(result[:success]).to be false
|
|
57
|
+
expect(result[:reason]).to eq(:insufficient_competitors)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns failure for empty proposal_ids' do
|
|
61
|
+
result = evolver.create_competition(gap: 'attention', proposal_ids: [])
|
|
62
|
+
expect(result[:success]).to be false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'creates competition in :pending status' do
|
|
66
|
+
cid = create_and_return_id
|
|
67
|
+
status = evolver.competition_status(competition_id: cid)
|
|
68
|
+
expect(status[:status]).to eq(:pending)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ─── run_trial ──────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
describe '.run_trial' do
|
|
75
|
+
let(:competition_id) { create_and_return_id }
|
|
76
|
+
|
|
77
|
+
it 'returns success: true with trial data' do
|
|
78
|
+
ext = build_extension(name: 'ext-a')
|
|
79
|
+
result = evolver.run_trial(competition_id: competition_id, extension: ext)
|
|
80
|
+
expect(result[:success]).to be true
|
|
81
|
+
expect(result[:trial][:extension_name]).to eq('ext-a')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'records the fitness score from FitnessEvaluator' do
|
|
85
|
+
ext = build_extension(name: 'ext-a', invocation_count: 1000, impact_score: 0.9, health_score: 1.0)
|
|
86
|
+
result = evolver.run_trial(competition_id: competition_id, extension: ext)
|
|
87
|
+
expect(result[:trial][:fitness]).to be > 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'transitions competition to :active on first trial' do
|
|
91
|
+
ext = build_extension(name: 'ext-a')
|
|
92
|
+
evolver.run_trial(competition_id: competition_id, extension: ext)
|
|
93
|
+
status = evolver.competition_status(competition_id: competition_id)
|
|
94
|
+
expect(status[:status]).to eq(:active)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns failure for nonexistent competition' do
|
|
98
|
+
ext = build_extension(name: 'ext-a')
|
|
99
|
+
result = evolver.run_trial(competition_id: 'bogus', extension: ext)
|
|
100
|
+
expect(result[:success]).to be false
|
|
101
|
+
expect(result[:reason]).to eq(:not_found)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns failure for decided competition' do
|
|
105
|
+
cid = create_and_return_id
|
|
106
|
+
ext_a = build_extension(name: 'ext-a', invocation_count: 1000, impact_score: 0.9)
|
|
107
|
+
ext_b = build_extension(name: 'ext-b', invocation_count: 10, impact_score: 0.1)
|
|
108
|
+
evolver.run_trial(competition_id: cid, extension: ext_a)
|
|
109
|
+
evolver.run_trial(competition_id: cid, extension: ext_b)
|
|
110
|
+
evolver.declare_winner(competition_id: cid)
|
|
111
|
+
|
|
112
|
+
result = evolver.run_trial(competition_id: cid, extension: ext_a)
|
|
113
|
+
expect(result[:success]).to be false
|
|
114
|
+
expect(result[:reason]).to eq(:already_decided)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'uses extension_name if name is not present' do
|
|
118
|
+
ext = { extension_name: 'ext-fallback', invocation_count: 50, error_rate: 0.0,
|
|
119
|
+
avg_latency_ms: 10, impact_score: 0.5, health_score: 0.7 }
|
|
120
|
+
result = evolver.run_trial(competition_id: competition_id, extension: ext)
|
|
121
|
+
expect(result[:trial][:extension_name]).to eq('ext-fallback')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'records custom iteration count' do
|
|
125
|
+
ext = build_extension(name: 'ext-a')
|
|
126
|
+
result = evolver.run_trial(competition_id: competition_id, extension: ext, iterations: 50)
|
|
127
|
+
expect(result[:trial][:iterations]).to eq(50)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ─── compare_results ────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe '.compare_results' do
|
|
134
|
+
let(:competition_id) { create_and_return_id }
|
|
135
|
+
|
|
136
|
+
it 'returns empty comparison when no trials exist' do
|
|
137
|
+
result = evolver.compare_results(competition_id: competition_id)
|
|
138
|
+
expect(result[:success]).to be true
|
|
139
|
+
expect(result[:comparison]).to be_empty
|
|
140
|
+
expect(result[:leader]).to be_nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'ranks extensions by fitness descending' do
|
|
144
|
+
ext_high = build_extension(name: 'winner', invocation_count: 1000, impact_score: 0.9, health_score: 1.0)
|
|
145
|
+
ext_low = build_extension(name: 'loser', invocation_count: 10, impact_score: 0.1, health_score: 0.3)
|
|
146
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_low)
|
|
147
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_high)
|
|
148
|
+
|
|
149
|
+
result = evolver.compare_results(competition_id: competition_id)
|
|
150
|
+
expect(result[:leader]).to eq('winner')
|
|
151
|
+
expect(result[:comparison].first[:rank]).to eq(1)
|
|
152
|
+
expect(result[:comparison].first[:extension_name]).to eq('winner')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'breaks ties by lower latency' do
|
|
156
|
+
ext_a = build_extension(name: 'fast', invocation_count: 100, impact_score: 0.7,
|
|
157
|
+
health_score: 0.8, avg_latency_ms: 10)
|
|
158
|
+
ext_b = build_extension(name: 'slow', invocation_count: 100, impact_score: 0.7,
|
|
159
|
+
health_score: 0.8, avg_latency_ms: 500)
|
|
160
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_a)
|
|
161
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_b)
|
|
162
|
+
|
|
163
|
+
result = evolver.compare_results(competition_id: competition_id)
|
|
164
|
+
expect(result[:leader]).to eq('fast')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'returns failure for nonexistent competition' do
|
|
168
|
+
result = evolver.compare_results(competition_id: 'bogus')
|
|
169
|
+
expect(result[:success]).to be false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'includes all trial data in comparison' do
|
|
173
|
+
ext = build_extension(name: 'solo', error_rate: 0.05, avg_latency_ms: 42)
|
|
174
|
+
evolver.run_trial(competition_id: competition_id, extension: ext)
|
|
175
|
+
result = evolver.compare_results(competition_id: competition_id)
|
|
176
|
+
entry = result[:comparison].first
|
|
177
|
+
expect(entry).to include(:fitness, :error_rate, :avg_latency_ms, :rank)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ─── declare_winner ─────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe '.declare_winner' do
|
|
184
|
+
let(:competition_id) { create_and_return_id }
|
|
185
|
+
|
|
186
|
+
before do
|
|
187
|
+
ext_a = build_extension(name: 'champion', invocation_count: 1000, impact_score: 0.9, health_score: 1.0)
|
|
188
|
+
ext_b = build_extension(name: 'challenger', invocation_count: 10, impact_score: 0.1, health_score: 0.3)
|
|
189
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_a)
|
|
190
|
+
evolver.run_trial(competition_id: competition_id, extension: ext_b)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'returns success: true with the winner' do
|
|
194
|
+
result = evolver.declare_winner(competition_id: competition_id)
|
|
195
|
+
expect(result[:success]).to be true
|
|
196
|
+
expect(result[:winner]).to eq('champion')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'returns the losers list' do
|
|
200
|
+
result = evolver.declare_winner(competition_id: competition_id)
|
|
201
|
+
expect(result[:losers]).to contain_exactly('challenger')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'transitions competition to :decided' do
|
|
205
|
+
evolver.declare_winner(competition_id: competition_id)
|
|
206
|
+
status = evolver.competition_status(competition_id: competition_id)
|
|
207
|
+
expect(status[:status]).to eq(:decided)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'sets the winner on the competition' do
|
|
211
|
+
evolver.declare_winner(competition_id: competition_id)
|
|
212
|
+
status = evolver.competition_status(competition_id: competition_id)
|
|
213
|
+
expect(status[:winner]).to eq('champion')
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'calls Evolver.replace_extension for each loser' do
|
|
217
|
+
expect(Legion::Extensions::MindGrowth::Runners::Evolver).to receive(:replace_extension)
|
|
218
|
+
.with(old_name: 'challenger', new_proposal_id: 'winner:champion')
|
|
219
|
+
.and_return({ success: true, replaced: 'challenger' })
|
|
220
|
+
evolver.declare_winner(competition_id: competition_id)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns failure for nonexistent competition' do
|
|
224
|
+
result = evolver.declare_winner(competition_id: 'bogus')
|
|
225
|
+
expect(result[:success]).to be false
|
|
226
|
+
expect(result[:reason]).to eq(:not_found)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'returns failure when already decided' do
|
|
230
|
+
evolver.declare_winner(competition_id: competition_id)
|
|
231
|
+
result = evolver.declare_winner(competition_id: competition_id)
|
|
232
|
+
expect(result[:success]).to be false
|
|
233
|
+
expect(result[:reason]).to eq(:already_decided)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'returns failure when no trials exist' do
|
|
237
|
+
cid = create_and_return_id(proposal_ids: %w[x y])
|
|
238
|
+
result = evolver.declare_winner(competition_id: cid)
|
|
239
|
+
expect(result[:success]).to be false
|
|
240
|
+
expect(result[:reason]).to eq(:no_trials)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# ─── competition_status ─────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
describe '.competition_status' do
|
|
247
|
+
it 'returns competition details' do
|
|
248
|
+
cid = create_and_return_id(gap: 'prediction', proposal_ids: %w[a b c])
|
|
249
|
+
result = evolver.competition_status(competition_id: cid)
|
|
250
|
+
expect(result[:success]).to be true
|
|
251
|
+
expect(result[:gap]).to eq('prediction')
|
|
252
|
+
expect(result[:competitors]).to eq(%w[a b c])
|
|
253
|
+
expect(result[:trial_count]).to eq(0)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it 'returns failure for nonexistent competition' do
|
|
257
|
+
result = evolver.competition_status(competition_id: 'missing')
|
|
258
|
+
expect(result[:success]).to be false
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# ─── active_competitions ────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
describe '.active_competitions' do
|
|
265
|
+
it 'returns empty list when no competitions exist' do
|
|
266
|
+
result = evolver.active_competitions
|
|
267
|
+
expect(result[:success]).to be true
|
|
268
|
+
expect(result[:count]).to eq(0)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'includes pending and active competitions' do
|
|
272
|
+
create_and_return_id(gap: 'gap1')
|
|
273
|
+
cid2 = create_and_return_id(gap: 'gap2')
|
|
274
|
+
evolver.run_trial(competition_id: cid2, extension: build_extension(name: 'e1'))
|
|
275
|
+
|
|
276
|
+
result = evolver.active_competitions
|
|
277
|
+
expect(result[:count]).to eq(2)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'excludes decided competitions' do
|
|
281
|
+
cid = create_and_return_id
|
|
282
|
+
evolver.run_trial(competition_id: cid, extension: build_extension(name: 'a', invocation_count: 1000))
|
|
283
|
+
evolver.run_trial(competition_id: cid, extension: build_extension(name: 'b', invocation_count: 1))
|
|
284
|
+
evolver.declare_winner(competition_id: cid)
|
|
285
|
+
|
|
286
|
+
result = evolver.active_competitions
|
|
287
|
+
expect(result[:count]).to eq(0)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# ─── competition_history ────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
describe '.competition_history' do
|
|
294
|
+
it 'returns empty list when no competitions exist' do
|
|
295
|
+
result = evolver.competition_history
|
|
296
|
+
expect(result[:success]).to be true
|
|
297
|
+
expect(result[:count]).to eq(0)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'returns competitions sorted by most recent first' do
|
|
301
|
+
create_and_return_id(gap: 'first')
|
|
302
|
+
_cid2 = create_and_return_id(gap: 'second')
|
|
303
|
+
|
|
304
|
+
result = evolver.competition_history
|
|
305
|
+
expect(result[:count]).to eq(2)
|
|
306
|
+
expect(result[:competitions].first[:gap]).to eq('second')
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it 'respects the limit parameter' do
|
|
310
|
+
3.times { |i| create_and_return_id(gap: "gap-#{i}") }
|
|
311
|
+
result = evolver.competition_history(limit: 2)
|
|
312
|
+
expect(result[:count]).to eq(2)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'includes winner and trial_count for decided competitions' do
|
|
316
|
+
cid = create_and_return_id
|
|
317
|
+
evolver.run_trial(competition_id: cid, extension: build_extension(name: 'w', invocation_count: 1000))
|
|
318
|
+
evolver.run_trial(competition_id: cid, extension: build_extension(name: 'l', invocation_count: 1))
|
|
319
|
+
evolver.declare_winner(competition_id: cid)
|
|
320
|
+
|
|
321
|
+
result = evolver.competition_history
|
|
322
|
+
entry = result[:competitions].find { |c| c[:id] == cid }
|
|
323
|
+
expect(entry[:winner]).to eq('w')
|
|
324
|
+
expect(entry[:trial_count]).to eq(2)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# ─── thread safety ─────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe 'thread safety' do
|
|
331
|
+
it 'handles concurrent competition creation' do
|
|
332
|
+
threads = 10.times.map do |i|
|
|
333
|
+
Thread.new { evolver.create_competition(gap: "gap-#{i}", proposal_ids: %w[a b]) }
|
|
334
|
+
end
|
|
335
|
+
results = threads.map(&:value)
|
|
336
|
+
expect(results.count { |r| r[:success] }).to eq(10)
|
|
337
|
+
expect(evolver.competition_history[:count]).to eq(10)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
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.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -144,6 +144,7 @@ files:
|
|
|
144
144
|
- lib/legion/extensions/mind_growth/helpers/proposal_store.rb
|
|
145
145
|
- lib/legion/extensions/mind_growth/runners/analyzer.rb
|
|
146
146
|
- lib/legion/extensions/mind_growth/runners/builder.rb
|
|
147
|
+
- lib/legion/extensions/mind_growth/runners/competitive_evolver.rb
|
|
147
148
|
- lib/legion/extensions/mind_growth/runners/composer.rb
|
|
148
149
|
- lib/legion/extensions/mind_growth/runners/consensus_builder.rb
|
|
149
150
|
- lib/legion/extensions/mind_growth/runners/dashboard.rb
|
|
@@ -171,6 +172,7 @@ files:
|
|
|
171
172
|
- spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb
|
|
172
173
|
- spec/legion/extensions/mind_growth/runners/analyzer_spec.rb
|
|
173
174
|
- spec/legion/extensions/mind_growth/runners/builder_spec.rb
|
|
175
|
+
- spec/legion/extensions/mind_growth/runners/competitive_evolver_spec.rb
|
|
174
176
|
- spec/legion/extensions/mind_growth/runners/composer_spec.rb
|
|
175
177
|
- spec/legion/extensions/mind_growth/runners/consensus_builder_spec.rb
|
|
176
178
|
- spec/legion/extensions/mind_growth/runners/dashboard_spec.rb
|