lex-mind-growth 0.1.7 → 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 +37 -0
- data/lib/legion/extensions/mind_growth/helpers/composition_map.rb +76 -0
- data/lib/legion/extensions/mind_growth/helpers/constants.rb +4 -0
- data/lib/legion/extensions/mind_growth/runners/composer.rb +118 -0
- data/lib/legion/extensions/mind_growth/runners/dashboard.rb +104 -0
- data/lib/legion/extensions/mind_growth/runners/dream_ideation.rb +120 -0
- data/lib/legion/extensions/mind_growth/runners/evolver.rb +169 -0
- data/lib/legion/extensions/mind_growth/runners/monitor.rb +103 -0
- data/lib/legion/extensions/mind_growth/version.rb +1 -1
- data/lib/legion/extensions/mind_growth.rb +6 -0
- data/spec/legion/extensions/mind_growth/helpers/composition_map_spec.rb +248 -0
- data/spec/legion/extensions/mind_growth/runners/composer_spec.rb +223 -0
- data/spec/legion/extensions/mind_growth/runners/dashboard_spec.rb +350 -0
- data/spec/legion/extensions/mind_growth/runners/dream_ideation_spec.rb +207 -0
- data/spec/legion/extensions/mind_growth/runners/evolver_spec.rb +357 -0
- data/spec/legion/extensions/mind_growth/runners/monitor_spec.rb +336 -0
- metadata +13 -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
|
|
@@ -47,6 +47,43 @@ module Legion
|
|
|
47
47
|
# RiskAssessor delegation
|
|
48
48
|
def assess_risk(**) = Runners::RiskAssessor.assess_risk(**)
|
|
49
49
|
def risk_summary(**) = Runners::RiskAssessor.risk_summary(**)
|
|
50
|
+
|
|
51
|
+
# Monitor delegation
|
|
52
|
+
def health_check(**) = Runners::Monitor.health_check(**)
|
|
53
|
+
def usage_stats(**) = Runners::Monitor.usage_stats(**)
|
|
54
|
+
def impact_score(**) = Runners::Monitor.impact_score(**)
|
|
55
|
+
def decay_check(**) = Runners::Monitor.decay_check(**)
|
|
56
|
+
def auto_prune(**) = Runners::Monitor.auto_prune(**)
|
|
57
|
+
def health_summary(**) = Runners::Monitor.health_summary(**)
|
|
58
|
+
|
|
59
|
+
# Composer delegation
|
|
60
|
+
def add_composition(**) = Runners::Composer.add_composition(**)
|
|
61
|
+
def remove_composition(**) = Runners::Composer.remove_composition(**)
|
|
62
|
+
def evaluate_output(**) = Runners::Composer.evaluate_output(**)
|
|
63
|
+
def composition_stats(**) = Runners::Composer.composition_stats(**)
|
|
64
|
+
def suggest_compositions(**) = Runners::Composer.suggest_compositions(**)
|
|
65
|
+
def list_compositions(**) = Runners::Composer.list_compositions(**)
|
|
66
|
+
|
|
67
|
+
# DreamIdeation delegation
|
|
68
|
+
def generate_dream_proposals(**) = Runners::DreamIdeation.generate_dream_proposals(**)
|
|
69
|
+
def dream_agenda_items(**) = Runners::DreamIdeation.dream_agenda_items(**)
|
|
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(**)
|
|
50
87
|
end
|
|
51
88
|
end
|
|
52
89
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Helpers
|
|
7
|
+
module CompositionMap
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
@rules = {}
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
|
|
13
|
+
def add_rule(source_extension:, output_key:, target_extension:, target_method:, transform: nil, **)
|
|
14
|
+
rule_id = SecureRandom.uuid
|
|
15
|
+
rule = {
|
|
16
|
+
id: rule_id,
|
|
17
|
+
source_extension: source_extension.to_s,
|
|
18
|
+
output_key: output_key.to_sym,
|
|
19
|
+
target_extension: target_extension.to_s,
|
|
20
|
+
target_method: target_method.to_sym,
|
|
21
|
+
transform: transform
|
|
22
|
+
}
|
|
23
|
+
@mutex.synchronize { @rules[rule_id] = rule }
|
|
24
|
+
{ success: true, rule_id: rule_id }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def remove_rule(rule_id:, **)
|
|
28
|
+
removed = @mutex.synchronize { @rules.delete(rule_id) }
|
|
29
|
+
{ success: !removed.nil?, rule_id: rule_id }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def rules_for(source_extension:, **)
|
|
33
|
+
src = source_extension.to_s
|
|
34
|
+
@mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def all_rules
|
|
38
|
+
@mutex.synchronize { @rules.values.dup }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def match_output(source_extension:, output:, **)
|
|
42
|
+
src = source_extension.to_s
|
|
43
|
+
out_h = output.is_a?(Hash) ? output : {}
|
|
44
|
+
rules = @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
|
|
45
|
+
|
|
46
|
+
rules.filter_map do |rule|
|
|
47
|
+
key = rule[:output_key]
|
|
48
|
+
next unless out_h.key?(key)
|
|
49
|
+
|
|
50
|
+
{ rule: rule, matched_value: out_h[key] }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear!
|
|
55
|
+
@mutex.synchronize { @rules.clear }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stats
|
|
59
|
+
all = @mutex.synchronize { @rules.values.dup }
|
|
60
|
+
|
|
61
|
+
by_source = Hash.new(0)
|
|
62
|
+
by_target = Hash.new(0)
|
|
63
|
+
all.each do |r|
|
|
64
|
+
by_source[r[:source_extension]] += 1
|
|
65
|
+
by_target[r[:target_extension]] += 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
{ total_rules: all.size,
|
|
69
|
+
by_source: by_source.transform_values { |v| v },
|
|
70
|
+
by_target: by_target.transform_values { |v| v } }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -54,6 +54,10 @@ module Legion
|
|
|
54
54
|
REJECTION_COOLDOWN_HOURS = 24
|
|
55
55
|
GOVERNANCE_STATUSES = %i[pending approved rejected expired].freeze
|
|
56
56
|
|
|
57
|
+
# Health monitoring
|
|
58
|
+
HEALTH_LEVELS = { excellent: 0.8, good: 0.6, fair: 0.4, degraded: 0.2, critical: 0.0 }.freeze
|
|
59
|
+
DECAY_INVOCATION_THRESHOLD = 5
|
|
60
|
+
|
|
57
61
|
# Risk assessment
|
|
58
62
|
RISK_TIERS = %i[low medium high critical].freeze
|
|
59
63
|
RISK_RECOMMENDATIONS = {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module Composer
|
|
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
|
+
# Category adjacency used for heuristic suggestions
|
|
14
|
+
CATEGORY_FLOW = [
|
|
15
|
+
%i[perception cognition],
|
|
16
|
+
%i[cognition memory],
|
|
17
|
+
%i[cognition introspection],
|
|
18
|
+
%i[memory cognition],
|
|
19
|
+
%i[introspection safety],
|
|
20
|
+
%i[motivation cognition],
|
|
21
|
+
%i[cognition communication],
|
|
22
|
+
%i[communication coordination]
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def add_composition(source_extension:, output_key:, target_extension:, target_method:,
|
|
26
|
+
transform: nil, **)
|
|
27
|
+
Helpers::CompositionMap.add_rule(
|
|
28
|
+
source_extension: source_extension,
|
|
29
|
+
output_key: output_key,
|
|
30
|
+
target_extension: target_extension,
|
|
31
|
+
target_method: target_method,
|
|
32
|
+
transform: transform
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_composition(rule_id:, **)
|
|
37
|
+
result = Helpers::CompositionMap.remove_rule(rule_id: rule_id)
|
|
38
|
+
{ success: result[:success] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def evaluate_output(source_extension:, output:, **)
|
|
42
|
+
matches = Helpers::CompositionMap.match_output(
|
|
43
|
+
source_extension: source_extension,
|
|
44
|
+
output: output
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
dispatches = matches.map do |match|
|
|
48
|
+
rule = match[:rule]
|
|
49
|
+
value = match[:matched_value]
|
|
50
|
+
input = rule[:transform] ? rule[:transform].call(value) : value
|
|
51
|
+
|
|
52
|
+
{ target_extension: rule[:target_extension],
|
|
53
|
+
target_method: rule[:target_method],
|
|
54
|
+
input: input }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
{ success: true, dispatches: dispatches, count: dispatches.size }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def composition_stats(**)
|
|
61
|
+
{ success: true, **Helpers::CompositionMap.stats }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def suggest_compositions(extensions:, **)
|
|
65
|
+
exts = Array(extensions)
|
|
66
|
+
|
|
67
|
+
return suggest_with_llm(exts) if defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
|
|
68
|
+
|
|
69
|
+
suggestions = heuristic_suggestions(exts)
|
|
70
|
+
{ success: true, suggestions: suggestions, count: suggestions.size }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def list_compositions(**)
|
|
74
|
+
rules = Helpers::CompositionMap.all_rules
|
|
75
|
+
{ success: true, rules: rules, count: rules.size }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def heuristic_suggestions(extensions)
|
|
81
|
+
ext_by_category = {}
|
|
82
|
+
extensions.each do |ext|
|
|
83
|
+
cat = (ext[:category] || :cognition).to_sym
|
|
84
|
+
(ext_by_category[cat] ||= []) << ext
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
suggestions = []
|
|
88
|
+
CATEGORY_FLOW.each do |src_cat, tgt_cat|
|
|
89
|
+
src_exts = ext_by_category[src_cat] || []
|
|
90
|
+
tgt_exts = ext_by_category[tgt_cat] || []
|
|
91
|
+
|
|
92
|
+
src_exts.each do |src|
|
|
93
|
+
tgt_exts.each do |tgt|
|
|
94
|
+
suggestions << {
|
|
95
|
+
source_extension: src[:name] || src[:extension_name],
|
|
96
|
+
output_key: :result,
|
|
97
|
+
target_extension: tgt[:name] || tgt[:extension_name],
|
|
98
|
+
target_method: :process,
|
|
99
|
+
rationale: "#{src_cat} -> #{tgt_cat} flow"
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
suggestions
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def suggest_with_llm(extensions)
|
|
109
|
+
suggestions = heuristic_suggestions(extensions)
|
|
110
|
+
{ success: true, suggestions: suggestions, count: suggestions.size }
|
|
111
|
+
rescue StandardError
|
|
112
|
+
{ success: true, suggestions: [], count: 0 }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
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,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module DreamIdeation
|
|
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
|
+
DREAM_NOVELTY_BONUS = 0.15
|
|
14
|
+
|
|
15
|
+
# Agenda item weight by how underrepresented the category is
|
|
16
|
+
MAX_AGENDA_WEIGHT = 1.0
|
|
17
|
+
MIN_AGENDA_WEIGHT = 0.1
|
|
18
|
+
|
|
19
|
+
def generate_dream_proposals(existing_extensions: nil, max_proposals: 2, **)
|
|
20
|
+
gap_result = Runners::Proposer.analyze_gaps(existing_extensions: existing_extensions)
|
|
21
|
+
return { success: false, error: :gap_analysis_failed } unless gap_result[:success]
|
|
22
|
+
|
|
23
|
+
recommendations = gap_result[:recommendations] || []
|
|
24
|
+
proposals = []
|
|
25
|
+
|
|
26
|
+
recommendations.first(max_proposals).each do |rec|
|
|
27
|
+
name = rec.is_a?(Hash) ? rec[:name] : rec.to_s
|
|
28
|
+
result = Runners::Proposer.propose_concept(
|
|
29
|
+
name: "lex-dream-#{name.to_s.downcase.gsub(/[^a-z0-9]/, '-')}",
|
|
30
|
+
description: "Dream-originated proposal for #{name} cognitive capability",
|
|
31
|
+
enrich: false
|
|
32
|
+
)
|
|
33
|
+
next unless result[:success]
|
|
34
|
+
|
|
35
|
+
proposal_id = result[:proposal][:id]
|
|
36
|
+
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
37
|
+
proposal&.instance_variable_set(:@origin, :dream)
|
|
38
|
+
|
|
39
|
+
proposals << result[:proposal]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
{ success: true, proposals: proposals, count: proposals.size,
|
|
43
|
+
gaps_analyzed: recommendations.size }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dream_agenda_items(existing_extensions: nil, **)
|
|
47
|
+
gap_result = Runners::Proposer.analyze_gaps(existing_extensions: existing_extensions)
|
|
48
|
+
return { success: false, error: :gap_analysis_failed } unless gap_result[:success]
|
|
49
|
+
|
|
50
|
+
target = Helpers::Constants::TARGET_DISTRIBUTION
|
|
51
|
+
models = gap_result[:models] || []
|
|
52
|
+
|
|
53
|
+
coverage_by_cat = build_coverage_by_category(models)
|
|
54
|
+
|
|
55
|
+
items = target.filter_map do |category, target_pct|
|
|
56
|
+
actual_pct = coverage_by_cat[category] || 0.0
|
|
57
|
+
gap = (target_pct - actual_pct).clamp(0.0, 1.0)
|
|
58
|
+
next if gap <= 0.0
|
|
59
|
+
|
|
60
|
+
weight = ((gap / target_pct) * MAX_AGENDA_WEIGHT).clamp(MIN_AGENDA_WEIGHT, MAX_AGENDA_WEIGHT).round(3)
|
|
61
|
+
|
|
62
|
+
{ type: :architectural_gap,
|
|
63
|
+
content: { gap_name: category, model: :target_distribution, coverage: actual_pct },
|
|
64
|
+
weight: weight }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
{ success: true, items: items, count: items.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def enrich_from_dream_context(proposal_id:, dream_context: {}, **)
|
|
71
|
+
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
72
|
+
return { success: false, error: :not_found } unless proposal
|
|
73
|
+
|
|
74
|
+
if dream_context && !dream_context.empty?
|
|
75
|
+
existing = proposal.rationale.to_s
|
|
76
|
+
additions = dream_context.map { |k, v| "#{k}: #{v}" }.join('; ')
|
|
77
|
+
new_rationale = existing.empty? ? additions : "#{existing}. Dream context: #{additions}"
|
|
78
|
+
proposal.instance_variable_set(:@rationale, new_rationale)
|
|
79
|
+
{ success: true, proposal_id: proposal_id, enriched: true }
|
|
80
|
+
else
|
|
81
|
+
{ success: true, proposal_id: proposal_id, enriched: false }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def build_coverage_by_category(models)
|
|
88
|
+
coverage = {}
|
|
89
|
+
models.each do |model|
|
|
90
|
+
cat = infer_category_from_model(model[:model])
|
|
91
|
+
next unless cat
|
|
92
|
+
|
|
93
|
+
existing = coverage[cat] || 1.0
|
|
94
|
+
coverage[cat] = [existing, model_coverage_fraction(model)].min
|
|
95
|
+
end
|
|
96
|
+
coverage
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def model_coverage_fraction(model)
|
|
100
|
+
total = model[:total_required] || 1
|
|
101
|
+
missing = (model[:missing] || []).size
|
|
102
|
+
covered = total - missing
|
|
103
|
+
total.positive? ? (covered.to_f / total).round(3) : 0.0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def infer_category_from_model(model_name)
|
|
107
|
+
mapping = {
|
|
108
|
+
global_workspace: :cognition,
|
|
109
|
+
free_energy: :introspection,
|
|
110
|
+
dual_process: :cognition,
|
|
111
|
+
somatic_marker: :motivation,
|
|
112
|
+
working_memory: :memory
|
|
113
|
+
}
|
|
114
|
+
mapping[model_name&.to_sym]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
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
|