lex-mind-growth 0.2.5 → 0.3.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 +4 -4
- data/Gemfile +1 -0
- data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +3 -3
- data/lib/legion/extensions/mind_growth/helpers/composition_map.rb +4 -4
- data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +24 -1
- data/lib/legion/extensions/mind_growth/helpers/phase_allocator.rb +1 -1
- data/lib/legion/extensions/mind_growth/helpers/proposal_persistence.rb +152 -0
- data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +39 -4
- data/lib/legion/extensions/mind_growth/hooks/governance_listener.rb +38 -0
- data/lib/legion/extensions/mind_growth/runners/builder.rb +72 -22
- data/lib/legion/extensions/mind_growth/runners/competitive_evolver.rb +3 -2
- data/lib/legion/extensions/mind_growth/runners/composer.rb +3 -3
- data/lib/legion/extensions/mind_growth/runners/consensus_builder.rb +4 -3
- data/lib/legion/extensions/mind_growth/runners/dashboard.rb +2 -2
- data/lib/legion/extensions/mind_growth/runners/dream_ideation.rb +2 -2
- data/lib/legion/extensions/mind_growth/runners/evolver.rb +15 -8
- data/lib/legion/extensions/mind_growth/runners/governance.rb +50 -4
- data/lib/legion/extensions/mind_growth/runners/integration_tester.rb +4 -3
- data/lib/legion/extensions/mind_growth/runners/monitor.rb +2 -2
- data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +105 -2
- data/lib/legion/extensions/mind_growth/runners/proposer.rb +30 -11
- data/lib/legion/extensions/mind_growth/runners/retrospective.rb +12 -7
- data/lib/legion/extensions/mind_growth/runners/risk_assessor.rb +2 -2
- data/lib/legion/extensions/mind_growth/runners/swarm_builder.rb +1 -1
- data/lib/legion/extensions/mind_growth/runners/wirer.rb +2 -2
- data/lib/legion/extensions/mind_growth/version.rb +1 -1
- data/lib/legion/extensions/mind_growth.rb +16 -1
- data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +2 -2
- data/spec/legion/extensions/mind_growth/helpers/composition_map_spec.rb +1 -1
- data/spec/legion/extensions/mind_growth/helpers/constants_spec.rb +47 -0
- data/spec/legion/extensions/mind_growth/helpers/proposal_persistence_spec.rb +120 -0
- data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +2 -2
- data/spec/legion/extensions/mind_growth/hooks/governance_callback_spec.rb +69 -0
- data/spec/legion/extensions/mind_growth/integration_spec.rb +140 -0
- data/spec/legion/extensions/mind_growth/runners/competitive_evolver_spec.rb +1 -1
- data/spec/legion/extensions/mind_growth/runners/governance_spec.rb +1 -1
- data/spec/legion/extensions/mind_growth/runners/post_build_pipeline_spec.rb +106 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fcaa84f5b0e455b0ac70019228ebe5bffdbd1c69228aafcad9bf92d7f91bb52
|
|
4
|
+
data.tar.gz: 4ea8b0c108d6bf0a957cd9709147e4cfaa3c78e0b297afc85e642331f9153e4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 911dc8e13b4dfe4cb4ee0cd5dd7cd27fc2d93a529aca350cc4d02d4f3f27b9548ac8881945bb57d150ffa8cc8e169e8f62b3590e2bbab4188dbfc0046aa69eb3
|
|
7
|
+
data.tar.gz: d4f849194085b3b398dd41e3499b686c87b323ab4950a392eaca71133aeb943fcfe68557829a5cb6be92428bf781c90ab53d49cf877f26abe5b699562c25d07e
|
data/Gemfile
CHANGED
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module MindGrowth
|
|
8
8
|
module Actor
|
|
9
|
-
class GrowthCycle < Legion::Extensions::Actors::Every
|
|
9
|
+
class GrowthCycle < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::MindGrowth::Runners::Orchestrator
|
|
12
12
|
end
|
|
@@ -19,7 +19,7 @@ module Legion
|
|
|
19
19
|
3600
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def enabled?
|
|
22
|
+
def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
|
|
23
23
|
codegen_loaded? || exec_loaded?
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -32,7 +32,7 @@ module Legion
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def check_subtask?
|
|
35
|
-
|
|
35
|
+
true
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def generate_task?
|
|
@@ -7,7 +7,7 @@ module Legion
|
|
|
7
7
|
module CompositionMap
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
@rules = {}
|
|
10
|
+
@rules = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
11
11
|
@mutex = Mutex.new
|
|
12
12
|
|
|
13
13
|
def add_rule(source_extension:, output_key:, target_extension:, target_method:, transform: nil, **)
|
|
@@ -19,7 +19,7 @@ module Legion
|
|
|
19
19
|
target_extension: target_extension.to_s,
|
|
20
20
|
target_method: target_method.to_sym,
|
|
21
21
|
transform: transform
|
|
22
|
-
}
|
|
22
|
+
}.freeze
|
|
23
23
|
@mutex.synchronize { @rules[rule_id] = rule }
|
|
24
24
|
{ success: true, rule_id: rule_id }
|
|
25
25
|
end
|
|
@@ -31,7 +31,7 @@ module Legion
|
|
|
31
31
|
|
|
32
32
|
def rules_for(source_extension:, **)
|
|
33
33
|
src = source_extension.to_s
|
|
34
|
-
@mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
|
|
34
|
+
@mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src }.map(&:dup) }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def all_rules
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
def match_output(source_extension:, output:, **)
|
|
42
42
|
src = source_extension.to_s
|
|
43
43
|
out_h = output.is_a?(Hash) ? output : {}
|
|
44
|
-
rules = @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
|
|
44
|
+
rules = @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src }.map(&:dup) }
|
|
45
45
|
|
|
46
46
|
rules.filter_map do |rule|
|
|
47
47
|
key = rule[:output_key]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module MindGrowth
|
|
@@ -12,7 +14,7 @@ module Legion
|
|
|
12
14
|
|
|
13
15
|
attr_reader(*FIELDS)
|
|
14
16
|
|
|
15
|
-
def initialize(name:, module_name:, category:, description:, metaphor: nil, helpers: [],
|
|
17
|
+
def initialize(name:, module_name:, category:, description:, metaphor: nil, helpers: [], # rubocop:disable Metrics/ParameterLists
|
|
16
18
|
runner_methods: [], rationale: nil, origin: :proposer)
|
|
17
19
|
@id = SecureRandom.uuid
|
|
18
20
|
@name = name
|
|
@@ -59,6 +61,27 @@ module Legion
|
|
|
59
61
|
@built_at = Time.now.utc if new_status == :passing
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
def self.from_h(hash)
|
|
65
|
+
h = hash.transform_keys(&:to_sym)
|
|
66
|
+
proposal = allocate
|
|
67
|
+
proposal.instance_variable_set(:@id, h[:id])
|
|
68
|
+
proposal.instance_variable_set(:@name, h[:name])
|
|
69
|
+
proposal.instance_variable_set(:@module_name, h[:module_name])
|
|
70
|
+
proposal.instance_variable_set(:@category, h[:category]&.to_sym)
|
|
71
|
+
proposal.instance_variable_set(:@description, h[:description])
|
|
72
|
+
proposal.instance_variable_set(:@metaphor, h[:metaphor])
|
|
73
|
+
proposal.instance_variable_set(:@helpers, h[:helpers] || [])
|
|
74
|
+
proposal.instance_variable_set(:@runner_methods, h[:runner_methods] || [])
|
|
75
|
+
proposal.instance_variable_set(:@rationale, h[:rationale])
|
|
76
|
+
proposal.instance_variable_set(:@scores, (h[:scores] || {}).transform_keys(&:to_sym))
|
|
77
|
+
proposal.instance_variable_set(:@status, h[:status]&.to_sym || :proposed)
|
|
78
|
+
proposal.instance_variable_set(:@origin, h[:origin]&.to_sym || :proposer)
|
|
79
|
+
proposal.instance_variable_set(:@created_at, h[:created_at] ? Time.parse(h[:created_at].to_s) : Time.now.utc)
|
|
80
|
+
proposal.instance_variable_set(:@evaluated_at, h[:evaluated_at] ? Time.parse(h[:evaluated_at].to_s) : nil)
|
|
81
|
+
proposal.instance_variable_set(:@built_at, h[:built_at] ? Time.parse(h[:built_at].to_s) : nil)
|
|
82
|
+
proposal
|
|
83
|
+
end
|
|
84
|
+
|
|
62
85
|
def to_h
|
|
63
86
|
FIELDS.to_h { |f| [f, send(f)] }
|
|
64
87
|
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Helpers
|
|
7
|
+
# Cache-backed persistence for proposals and governance votes.
|
|
8
|
+
# Best-effort: degrades to in-memory-only when cache is unavailable.
|
|
9
|
+
class ProposalPersistence
|
|
10
|
+
PROPOSAL_TTL = 604_800 # 7 days
|
|
11
|
+
VOTES_KEY_SUFFIX = ':votes'
|
|
12
|
+
INDEX_KEY_SUFFIX = ':index'
|
|
13
|
+
|
|
14
|
+
def initialize(namespace: 'legion_mind_growth')
|
|
15
|
+
@namespace = namespace
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save_proposal(proposal_hash)
|
|
19
|
+
return false unless cache_available?
|
|
20
|
+
|
|
21
|
+
key = proposal_key(proposal_hash[:id])
|
|
22
|
+
Legion::Cache.set_sync(key, serialize(proposal_hash), ttl: PROPOSAL_TTL)
|
|
23
|
+
update_index(proposal_hash[:id], :add)
|
|
24
|
+
true
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
log.error "[proposal_persistence] save_proposal failed for #{proposal_hash[:id]}: #{e.message}"
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_proposal(id)
|
|
31
|
+
return nil unless cache_available?
|
|
32
|
+
|
|
33
|
+
raw = Legion::Cache.get(proposal_key(id)) # rubocop:disable Legion/HelperMigration/DirectCache
|
|
34
|
+
return nil unless raw
|
|
35
|
+
|
|
36
|
+
deserialize(raw)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
log.error "[proposal_persistence] load_proposal failed for #{id}: #{e.message}"
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete_proposal(id)
|
|
43
|
+
return false unless cache_available?
|
|
44
|
+
|
|
45
|
+
Legion::Cache.delete_sync(proposal_key(id))
|
|
46
|
+
update_index(id, :remove)
|
|
47
|
+
true
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
log.error "[proposal_persistence] delete_proposal failed for #{id}: #{e.message}"
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_all_proposals
|
|
54
|
+
return {} unless cache_available?
|
|
55
|
+
|
|
56
|
+
ids = load_index
|
|
57
|
+
return {} if ids.empty?
|
|
58
|
+
|
|
59
|
+
ids.each_with_object({}) do |id, result|
|
|
60
|
+
p = load_proposal(id)
|
|
61
|
+
result[id] = p if p
|
|
62
|
+
end
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
log.error "[proposal_persistence] load_all_proposals failed: #{e.message}"
|
|
65
|
+
{}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete_all_proposals
|
|
69
|
+
return false unless cache_available?
|
|
70
|
+
|
|
71
|
+
ids = load_index
|
|
72
|
+
ids.each { |id| Legion::Cache.delete_sync(proposal_key(id)) }
|
|
73
|
+
Legion::Cache.delete_sync(index_key)
|
|
74
|
+
true
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
log.error "[proposal_persistence] delete_all_proposals failed: #{e.message}"
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def save_votes(votes_hash)
|
|
81
|
+
return false unless cache_available?
|
|
82
|
+
|
|
83
|
+
Legion::Cache.set_sync(votes_key, serialize(votes_hash), ttl: PROPOSAL_TTL)
|
|
84
|
+
true
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
log.error "[proposal_persistence] save_votes failed: #{e.message}"
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_votes
|
|
91
|
+
return {} unless cache_available?
|
|
92
|
+
|
|
93
|
+
raw = Legion::Cache.get(votes_key) # rubocop:disable Legion/HelperMigration/DirectCache
|
|
94
|
+
return {} unless raw
|
|
95
|
+
|
|
96
|
+
result = deserialize(raw)
|
|
97
|
+
result.is_a?(Hash) ? result : {}
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
log.error "[proposal_persistence] load_votes failed: #{e.message}"
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def cache_available?
|
|
106
|
+
defined?(Legion::Cache) && Legion::Cache.connected? # rubocop:disable Legion/HelperMigration/DirectCache
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def proposal_key(id) = "#{@namespace}:proposal:#{id}"
|
|
110
|
+
def votes_key = "#{@namespace}#{VOTES_KEY_SUFFIX}"
|
|
111
|
+
def index_key = "#{@namespace}#{INDEX_KEY_SUFFIX}"
|
|
112
|
+
|
|
113
|
+
def update_index(id, operation)
|
|
114
|
+
ids = load_index
|
|
115
|
+
case operation
|
|
116
|
+
when :add then ids << id unless ids.include?(id)
|
|
117
|
+
when :remove then ids.delete(id)
|
|
118
|
+
end
|
|
119
|
+
Legion::Cache.set_sync(index_key, serialize(ids), ttl: PROPOSAL_TTL)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def load_index
|
|
123
|
+
raw = Legion::Cache.get(index_key) # rubocop:disable Legion/HelperMigration/DirectCache
|
|
124
|
+
return [] unless raw
|
|
125
|
+
|
|
126
|
+
result = deserialize(raw)
|
|
127
|
+
result.is_a?(Array) ? result : []
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
log.error "[proposal_persistence] load_index failed: #{e.message}"
|
|
130
|
+
[]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def serialize(obj)
|
|
134
|
+
Legion::JSON.dump(obj) # rubocop:disable Legion/HelperMigration/DirectJson
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def deserialize(raw)
|
|
138
|
+
return raw if raw.is_a?(Hash) || raw.is_a?(Array)
|
|
139
|
+
|
|
140
|
+
Legion::JSON.load(raw) # rubocop:disable Legion/HelperMigration/DirectJson
|
|
141
|
+
rescue StandardError => _e
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def log
|
|
146
|
+
Legion::Logging
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -7,15 +7,18 @@ module Legion
|
|
|
7
7
|
class ProposalStore
|
|
8
8
|
MAX_PROPOSALS = 500
|
|
9
9
|
|
|
10
|
-
def initialize
|
|
11
|
-
@proposals
|
|
12
|
-
@mutex
|
|
10
|
+
def initialize(rehydrate: true)
|
|
11
|
+
@proposals = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@persistence = ProposalPersistence.new
|
|
14
|
+
rehydrate_from_cache if rehydrate
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def store(proposal)
|
|
16
18
|
@mutex.synchronize do
|
|
17
19
|
evict_oldest if @proposals.size >= MAX_PROPOSALS
|
|
18
20
|
@proposals[proposal.id] = proposal
|
|
21
|
+
@persistence.save_proposal(proposal.to_h)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
@@ -23,6 +26,13 @@ module Legion
|
|
|
23
26
|
@mutex.synchronize { @proposals[id] }
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
def update(proposal)
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@proposals[proposal.id] = proposal
|
|
32
|
+
@persistence.save_proposal(proposal.to_h)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
def all
|
|
27
37
|
@mutex.synchronize { @proposals.values.dup }
|
|
28
38
|
end
|
|
@@ -58,11 +68,36 @@ module Legion
|
|
|
58
68
|
@mutex.synchronize { @proposals.clear }
|
|
59
69
|
end
|
|
60
70
|
|
|
71
|
+
def clear_persisted!
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
@proposals.clear
|
|
74
|
+
@persistence.delete_all_proposals
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
private
|
|
62
79
|
|
|
63
80
|
def evict_oldest
|
|
64
81
|
oldest = @proposals.values.min_by { |p| p.created_at.to_f }
|
|
65
|
-
|
|
82
|
+
return unless oldest
|
|
83
|
+
|
|
84
|
+
@proposals.delete(oldest.id)
|
|
85
|
+
@persistence.delete_proposal(oldest.id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rehydrate_from_cache
|
|
89
|
+
cached = @persistence.load_all_proposals
|
|
90
|
+
cached.each do |id, hash|
|
|
91
|
+
proposal = ConceptProposal.from_h(hash)
|
|
92
|
+
@proposals[id] = proposal
|
|
93
|
+
end
|
|
94
|
+
log.info "[proposal_store] rehydrated #{cached.size} proposals from cache" unless cached.empty?
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
log.error "[proposal_store] rehydrate_from_cache failed: #{e.message}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def log
|
|
100
|
+
Legion::Logging
|
|
66
101
|
end
|
|
67
102
|
end
|
|
68
103
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Hooks
|
|
7
|
+
module GovernanceListener
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def register
|
|
11
|
+
return unless defined?(Legion::Events)
|
|
12
|
+
|
|
13
|
+
Legion::Events.on('governance.quorum_reached') do |event|
|
|
14
|
+
next unless event[:verdict] == :approved
|
|
15
|
+
|
|
16
|
+
base_path = event[:base_path] || GovernanceListener.configured_base_path
|
|
17
|
+
GovernanceListener.log.info "[governance_listener] quorum approved for #{event[:proposal_id]}, triggering build"
|
|
18
|
+
Runners::Governance.governance_resolved(proposal_id: event[:proposal_id], base_path: base_path)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configured_base_path
|
|
23
|
+
return nil unless defined?(Legion::Settings)
|
|
24
|
+
|
|
25
|
+
Legion::Settings.dig(:mind_growth, :base_path)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
log.debug "[governance_listener] configured_base_path failed: #{e.message}"
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log
|
|
32
|
+
Legion::Logging
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -5,8 +5,8 @@ module Legion
|
|
|
5
5
|
module MindGrowth
|
|
6
6
|
module Runners
|
|
7
7
|
module Builder
|
|
8
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
10
10
|
|
|
11
11
|
extend self
|
|
12
12
|
|
|
@@ -16,6 +16,7 @@ module Legion
|
|
|
16
16
|
|
|
17
17
|
pipeline = Helpers::BuildPipeline.new(proposal)
|
|
18
18
|
proposal.transition!(:building)
|
|
19
|
+
persist_proposal(proposal)
|
|
19
20
|
base_path ||= ::Dir.pwd
|
|
20
21
|
|
|
21
22
|
run_stage(pipeline, :scaffold, -> { scaffold_stage(proposal, base_path) })
|
|
@@ -25,6 +26,7 @@ module Legion
|
|
|
25
26
|
run_stage(pipeline, :register, -> { register_stage(proposal) }) unless pipeline.failed?
|
|
26
27
|
|
|
27
28
|
proposal.transition!(pipeline.complete? ? :passing : :build_failed)
|
|
29
|
+
persist_proposal(proposal)
|
|
28
30
|
log.info "[mind_growth:builder] #{proposal.name}: #{pipeline.stage}"
|
|
29
31
|
{ success: pipeline.complete?, pipeline: pipeline.to_h, proposal: proposal.to_h }
|
|
30
32
|
rescue ArgumentError => e
|
|
@@ -46,6 +48,12 @@ module Legion
|
|
|
46
48
|
Runners::Proposer.get_proposal_object(proposal_id)
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def persist_proposal(proposal)
|
|
52
|
+
return unless defined?(Runners::Proposer) && Runners::Proposer.respond_to?(:persist_proposal)
|
|
53
|
+
|
|
54
|
+
Runners::Proposer.persist_proposal(proposal)
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
def run_stage(pipeline, stage, callable)
|
|
50
58
|
return if pipeline.stage != stage
|
|
51
59
|
|
|
@@ -59,13 +67,21 @@ module Legion
|
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
def strip_lex_prefix(name)
|
|
62
|
-
name.to_s.
|
|
70
|
+
name.to_s.delete_prefix('lex-')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def proposal_id_for(proposal)
|
|
74
|
+
proposal.id if proposal.respond_to?(:id)
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
# --- Scaffold Stage ---
|
|
66
78
|
# Delegates to lex-codegen when loaded; stubs otherwise
|
|
67
79
|
def scaffold_stage(proposal, base_path)
|
|
68
|
-
|
|
80
|
+
unless codegen_available?
|
|
81
|
+
return { success: true, stage: :scaffold, files: 0, message: 'scaffold requires lex-codegen',
|
|
82
|
+
proposal_id: proposal_id_for(proposal),
|
|
83
|
+
path: ext_path(proposal, base_path) }
|
|
84
|
+
end
|
|
69
85
|
|
|
70
86
|
name = strip_lex_prefix(proposal.name)
|
|
71
87
|
result = Legion::Extensions::Codegen::Runners::Generate.scaffold_extension(
|
|
@@ -79,14 +95,18 @@ module Legion
|
|
|
79
95
|
)
|
|
80
96
|
|
|
81
97
|
{ success: result[:success], stage: :scaffold, files: result[:files_created] || 0,
|
|
82
|
-
path: result[:path], error: result[:error]
|
|
98
|
+
path: result[:path] || ext_path(proposal, base_path), error: result[:error],
|
|
99
|
+
proposal_id: proposal_id_for(proposal) }
|
|
83
100
|
end
|
|
84
101
|
|
|
85
102
|
# --- Implement Stage ---
|
|
86
103
|
# Delegates to lex-codegen FromGap when loaded; falls back to legion-llm; stubs otherwise
|
|
87
104
|
def implement_stage(proposal, base_path)
|
|
88
105
|
unless llm_available? || codegen_from_gap_available?
|
|
89
|
-
return { success: true, stage: :implement,
|
|
106
|
+
return { success: true, stage: :implement,
|
|
107
|
+
message: 'implementation requires legion-llm or lex-codegen',
|
|
108
|
+
proposal_id: proposal_id_for(proposal),
|
|
109
|
+
path: ext_path(proposal, base_path) }
|
|
90
110
|
end
|
|
91
111
|
|
|
92
112
|
path = ext_path(proposal, base_path)
|
|
@@ -94,7 +114,9 @@ module Legion
|
|
|
94
114
|
|
|
95
115
|
if target_files.empty?
|
|
96
116
|
return { success: true, stage: :implement, files_implemented: 0,
|
|
97
|
-
message: 'no implementation targets found'
|
|
117
|
+
message: 'no implementation targets found',
|
|
118
|
+
proposal_id: proposal_id_for(proposal),
|
|
119
|
+
path: path }
|
|
98
120
|
end
|
|
99
121
|
|
|
100
122
|
files_implemented = 0
|
|
@@ -111,43 +133,64 @@ module Legion
|
|
|
111
133
|
|
|
112
134
|
success = errors.empty?
|
|
113
135
|
{ success: success, stage: :implement, files_implemented: files_implemented,
|
|
114
|
-
total_files: target_files.size, error: success ? nil : errors.join('; ')
|
|
136
|
+
total_files: target_files.size, error: success ? nil : errors.join('; '),
|
|
137
|
+
proposal_id: proposal_id_for(proposal),
|
|
138
|
+
path: path }
|
|
115
139
|
end
|
|
116
140
|
|
|
117
141
|
# --- Test Stage ---
|
|
118
142
|
# Delegates to lex-exec bundler runners when loaded; stubs otherwise
|
|
119
143
|
def test_stage(proposal, base_path)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
unless exec_available?
|
|
145
|
+
return { success: true, stage: :test, message: 'testing requires lex-exec',
|
|
146
|
+
proposal_id: proposal_id_for(proposal),
|
|
147
|
+
path: ext_path(proposal, base_path) }
|
|
148
|
+
end
|
|
123
149
|
|
|
150
|
+
path = ext_path(proposal, base_path)
|
|
151
|
+
pid = proposal_id_for(proposal)
|
|
124
152
|
install = Legion::Extensions::Exec::Runners::Bundler.install(path: path)
|
|
125
153
|
unless install[:success]
|
|
126
154
|
return { success: false, stage: :test, step: :install,
|
|
127
|
-
error: install[:stderr] || install[:error]
|
|
155
|
+
error: install[:stderr] || install[:error],
|
|
156
|
+
proposal_id: pid, path: path }
|
|
128
157
|
end
|
|
129
158
|
|
|
159
|
+
run_test_suite(path: path, pid: pid)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def run_test_suite(path:, pid:)
|
|
130
163
|
rspec = Legion::Extensions::Exec::Runners::Bundler.exec_rspec(path: path)
|
|
131
164
|
rubocop = Legion::Extensions::Exec::Runners::Bundler.exec_rubocop(path: path)
|
|
132
165
|
|
|
133
166
|
rspec_ok = rspec[:success] && (rspec.dig(:parsed, :failures) || 0).zero?
|
|
134
167
|
rubocop_ok = rubocop[:success]
|
|
135
168
|
|
|
136
|
-
errors =
|
|
137
|
-
(rspec_ok ? nil : "rspec: #{rspec[:parsed] || rspec[:stderr]}"),
|
|
138
|
-
(rubocop_ok ? nil : "rubocop: #{rubocop[:parsed] || rubocop[:stderr]}")
|
|
139
|
-
].compact.join('; ')
|
|
169
|
+
errors = build_test_errors(rspec: rspec, rubocop: rubocop, rspec_ok: rspec_ok, rubocop_ok: rubocop_ok)
|
|
140
170
|
|
|
141
171
|
{ success: rspec_ok && rubocop_ok, stage: :test,
|
|
142
172
|
rspec: rspec[:parsed] || { raw: rspec[:stdout] },
|
|
143
173
|
rubocop: rubocop[:parsed] || { raw: rubocop[:stdout] },
|
|
144
|
-
error: errors.empty? ? nil : errors
|
|
174
|
+
error: errors.empty? ? nil : errors,
|
|
175
|
+
proposal_id: pid,
|
|
176
|
+
path: path }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_test_errors(rspec:, rubocop:, rspec_ok:, rubocop_ok:)
|
|
180
|
+
[
|
|
181
|
+
(rspec_ok ? nil : "rspec: #{rspec[:parsed] || rspec[:stderr]}"),
|
|
182
|
+
(rubocop_ok ? nil : "rubocop: #{rubocop[:parsed] || rubocop[:stderr]}")
|
|
183
|
+
].compact.join('; ')
|
|
145
184
|
end
|
|
146
185
|
|
|
147
186
|
# --- Validate Stage ---
|
|
148
187
|
# Delegates to lex-codegen validators when loaded; stubs otherwise
|
|
149
188
|
def validate_stage(proposal, base_path)
|
|
150
|
-
|
|
189
|
+
unless codegen_available?
|
|
190
|
+
return { success: true, stage: :validate, message: 'validation requires lex-codegen',
|
|
191
|
+
proposal_id: proposal_id_for(proposal),
|
|
192
|
+
path: ext_path(proposal, base_path) }
|
|
193
|
+
end
|
|
151
194
|
|
|
152
195
|
path = ext_path(proposal, base_path)
|
|
153
196
|
structure = Legion::Extensions::Codegen::Runners::Validate.validate_structure(path: path)
|
|
@@ -155,13 +198,19 @@ module Legion
|
|
|
155
198
|
|
|
156
199
|
valid = structure[:valid] && gemspec[:valid]
|
|
157
200
|
{ success: valid, stage: :validate, structure: structure, gemspec: gemspec,
|
|
158
|
-
error: valid ? nil : "structure: #{structure[:missing]}, gemspec: #{gemspec[:issues]}"
|
|
201
|
+
error: valid ? nil : "structure: #{structure[:missing]}, gemspec: #{gemspec[:issues]}",
|
|
202
|
+
proposal_id: proposal_id_for(proposal),
|
|
203
|
+
path: path }
|
|
159
204
|
end
|
|
160
205
|
|
|
161
206
|
# --- Register Stage ---
|
|
162
207
|
# Delegates to lex-metacognition registry when loaded; stubs otherwise
|
|
163
208
|
def register_stage(proposal)
|
|
164
|
-
|
|
209
|
+
unless registry_available?
|
|
210
|
+
return { success: true, stage: :register,
|
|
211
|
+
message: 'registration requires lex-metacognition registry',
|
|
212
|
+
proposal_id: proposal_id_for(proposal) }
|
|
213
|
+
end
|
|
165
214
|
|
|
166
215
|
result = Legion::Extensions::Metacognition::Runners::Registry.register_extension(
|
|
167
216
|
name: proposal.name,
|
|
@@ -170,7 +219,8 @@ module Legion
|
|
|
170
219
|
description: proposal.description
|
|
171
220
|
)
|
|
172
221
|
|
|
173
|
-
{ success: result[:success], stage: :register, error: result[:error]
|
|
222
|
+
{ success: result[:success], stage: :register, error: result[:error],
|
|
223
|
+
proposal_id: proposal_id_for(proposal) }
|
|
174
224
|
end
|
|
175
225
|
|
|
176
226
|
# --- LLM implementation helpers ---
|
|
@@ -205,7 +255,7 @@ module Legion
|
|
|
205
255
|
def legacy_implement_file(file_path, proposal)
|
|
206
256
|
stub_content = ::File.read(file_path)
|
|
207
257
|
|
|
208
|
-
chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning })
|
|
258
|
+
chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning }) # rubocop:disable Legion/HelperMigration/DirectLlm
|
|
209
259
|
chat.with_instructions(implementation_instructions)
|
|
210
260
|
response = chat.ask(file_implementation_prompt(stub_content, proposal))
|
|
211
261
|
code = extract_ruby_code(response.content)
|
|
@@ -7,7 +7,8 @@ module Legion
|
|
|
7
7
|
module CompetitiveEvolver
|
|
8
8
|
extend self
|
|
9
9
|
|
|
10
|
-
COMPETITION_STATUSES
|
|
10
|
+
COMPETITION_STATUSES = %i[pending active evaluating decided cancelled].freeze
|
|
11
|
+
ACTIVE_STATUSES = %i[pending active evaluating].freeze
|
|
11
12
|
MIN_TRIAL_ITERATIONS = 10
|
|
12
13
|
|
|
13
14
|
def create_competition(gap:, proposal_ids:, **)
|
|
@@ -108,7 +109,7 @@ module Legion
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def active_competitions(**)
|
|
111
|
-
comps = all_competitions.select { |c|
|
|
112
|
+
comps = all_competitions.select { |c| ACTIVE_STATUSES.include?(c[:status]) }
|
|
112
113
|
{ success: true, competitions: comps.map { |c| { id: c[:id], gap: c[:gap], status: c[:status] } },
|
|
113
114
|
count: comps.size }
|
|
114
115
|
end
|
|
@@ -5,8 +5,8 @@ module Legion
|
|
|
5
5
|
module MindGrowth
|
|
6
6
|
module Runners
|
|
7
7
|
module Composer
|
|
8
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
10
10
|
|
|
11
11
|
extend self
|
|
12
12
|
|
|
@@ -108,7 +108,7 @@ module Legion
|
|
|
108
108
|
def suggest_with_llm(extensions)
|
|
109
109
|
suggestions = heuristic_suggestions(extensions)
|
|
110
110
|
{ success: true, suggestions: suggestions, count: suggestions.size }
|
|
111
|
-
rescue StandardError
|
|
111
|
+
rescue StandardError => _e
|
|
112
112
|
{ success: true, suggestions: [], count: 0 }
|
|
113
113
|
end
|
|
114
114
|
end
|