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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2182f87f0f59f727c61f209083c225b1b31d9cbb5589af437618350433e8b692
4
- data.tar.gz: 1ee0428c83686efdb168054b16d68af66443dfa60bd5f0bd8e2725f4d7d87ab4
3
+ metadata.gz: 647d99aa35555ba0ff36f584ad78d0c2104a9c09e83d72fbee7b897d097c7380
4
+ data.tar.gz: b7803d7ea950fb03154b695b5631f8a2e523adfad0df4963f8a22bf98e952f67
5
5
  SHA512:
6
- metadata.gz: 1e7320c15ecd31d531bd961b03f20baa8c30a77946f98e58c25557ebcd1027b42b7c9af7c9ce8ae781089e25966a76d183209922ce8b2e7a780c141853cf6d96
7
- data.tar.gz: b7abcbc0ea3109d64a4cc1cda9508ea0ee8ab777271e205abc1ae4903d1f2bf349b78053aa071fe5835790a790f4974be95800cf57ad2bfa8ab96182b358d312
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