lex-mind-growth 0.1.6 → 0.1.8
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 +33 -0
- data/lib/legion/extensions/mind_growth/helpers/composition_map.rb +76 -0
- data/lib/legion/extensions/mind_growth/helpers/constants.rb +18 -0
- data/lib/legion/extensions/mind_growth/runners/composer.rb +118 -0
- data/lib/legion/extensions/mind_growth/runners/dream_ideation.rb +120 -0
- data/lib/legion/extensions/mind_growth/runners/governance.rb +122 -0
- data/lib/legion/extensions/mind_growth/runners/monitor.rb +103 -0
- data/lib/legion/extensions/mind_growth/runners/risk_assessor.rb +106 -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/dream_ideation_spec.rb +207 -0
- data/spec/legion/extensions/mind_growth/runners/governance_spec.rb +439 -0
- data/spec/legion/extensions/mind_growth/runners/monitor_spec.rb +336 -0
- data/spec/legion/extensions/mind_growth/runners/risk_assessor_spec.rb +395 -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: 87d35372e2ef409b603807723d816df60439d40bc8f4289d48799d31b731d232
|
|
4
|
+
data.tar.gz: fd49c0c8bb06f49374cbbb3afb497f45ee58193e71ff66b1ef1c622b94bbeaa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3c07088c0173a6e5669af58bf3fbd315f2ef9098fa9e3f3f65228815159e6a061860c05aba14b75a0f0dcd9f01aea50f5a89ec9455a5796484037c6e1571328
|
|
7
|
+
data.tar.gz: 98b9b07557efbb5a3546a4018300ab984a0bfecc24ae93ab2534c35c90b991877d5564863460a75137525b262c996b7536ca14c6bbd20f77671225a8501ba402
|
|
@@ -35,6 +35,39 @@ module Legion
|
|
|
35
35
|
def session_report(**) = Runners::Retrospective.session_report(**)
|
|
36
36
|
def trend_analysis(**) = Runners::Retrospective.trend_analysis(**)
|
|
37
37
|
def learning_extraction(**) = Runners::Retrospective.learning_extraction(**)
|
|
38
|
+
|
|
39
|
+
# Governance delegation
|
|
40
|
+
def submit_proposal(**) = Runners::Governance.submit_proposal(**)
|
|
41
|
+
def vote_on_proposal(**) = Runners::Governance.vote_on_proposal(**)
|
|
42
|
+
def tally_votes(**) = Runners::Governance.tally_votes(**)
|
|
43
|
+
def approve_proposal(**) = Runners::Governance.approve_proposal(**)
|
|
44
|
+
def reject_proposal(**) = Runners::Governance.reject_proposal(**)
|
|
45
|
+
def governance_stats(**) = Runners::Governance.governance_stats(**)
|
|
46
|
+
|
|
47
|
+
# RiskAssessor delegation
|
|
48
|
+
def assess_risk(**) = Runners::RiskAssessor.assess_risk(**)
|
|
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(**)
|
|
38
71
|
end
|
|
39
72
|
end
|
|
40
73
|
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
|
|
@@ -48,6 +48,24 @@ module Legion
|
|
|
48
48
|
|
|
49
49
|
# Reference cognitive models
|
|
50
50
|
COGNITIVE_MODELS = %i[global_workspace free_energy dual_process somatic_marker working_memory].freeze
|
|
51
|
+
|
|
52
|
+
# Governance
|
|
53
|
+
QUORUM = 3
|
|
54
|
+
REJECTION_COOLDOWN_HOURS = 24
|
|
55
|
+
GOVERNANCE_STATUSES = %i[pending approved rejected expired].freeze
|
|
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
|
+
|
|
61
|
+
# Risk assessment
|
|
62
|
+
RISK_TIERS = %i[low medium high critical].freeze
|
|
63
|
+
RISK_RECOMMENDATIONS = {
|
|
64
|
+
low: :auto_approve,
|
|
65
|
+
medium: :governance,
|
|
66
|
+
high: :human_required,
|
|
67
|
+
critical: :blocked
|
|
68
|
+
}.freeze
|
|
51
69
|
end
|
|
52
70
|
end
|
|
53
71
|
end
|
|
@@ -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,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,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module Governance
|
|
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
|
+
VOTE_VALUES = %i[approve reject].freeze
|
|
14
|
+
|
|
15
|
+
def submit_proposal(proposal_id:, **)
|
|
16
|
+
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
17
|
+
return { success: false, error: :not_found } unless proposal
|
|
18
|
+
|
|
19
|
+
return { success: false, error: :invalid_status, current_status: proposal.status } unless %i[proposed evaluating].include?(proposal.status)
|
|
20
|
+
|
|
21
|
+
proposal.transition!(:evaluating)
|
|
22
|
+
{ success: true, proposal_id: proposal_id, status: :evaluating }
|
|
23
|
+
rescue ArgumentError => e
|
|
24
|
+
{ success: false, error: e.message }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def vote_on_proposal(proposal_id:, vote:, agent_id: 'default', rationale: nil, **)
|
|
28
|
+
vote_sym = vote.to_sym
|
|
29
|
+
return { success: false, error: :invalid_vote } unless VOTE_VALUES.include?(vote_sym)
|
|
30
|
+
|
|
31
|
+
votes_mutex.synchronize do
|
|
32
|
+
votes_store[proposal_id] ||= []
|
|
33
|
+
votes_store[proposal_id] << { vote: vote_sym, agent_id: agent_id.to_s, rationale: rationale,
|
|
34
|
+
cast_at: Time.now.utc }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ success: true, proposal_id: proposal_id, vote: vote_sym, agent_id: agent_id.to_s }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tally_votes(proposal_id:, **)
|
|
41
|
+
ballots = votes_mutex.synchronize { (votes_store[proposal_id] || []).dup }
|
|
42
|
+
|
|
43
|
+
approve_count = ballots.count { |b| b[:vote] == :approve }
|
|
44
|
+
reject_count = ballots.count { |b| b[:vote] == :reject }
|
|
45
|
+
total = ballots.size
|
|
46
|
+
|
|
47
|
+
verdict = if total < Helpers::Constants::QUORUM
|
|
48
|
+
:pending
|
|
49
|
+
elsif approve_count > reject_count
|
|
50
|
+
:approved
|
|
51
|
+
else
|
|
52
|
+
:rejected
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ success: true, proposal_id: proposal_id, approve_count: approve_count,
|
|
56
|
+
reject_count: reject_count, total: total, verdict: verdict }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def approve_proposal(proposal_id:, _reason: nil, **)
|
|
60
|
+
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
61
|
+
return { success: false, error: :not_found } unless proposal
|
|
62
|
+
|
|
63
|
+
proposal.transition!(:approved)
|
|
64
|
+
{ success: true, proposal_id: proposal_id, status: :approved }
|
|
65
|
+
rescue ArgumentError => e
|
|
66
|
+
{ success: false, error: e.message }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reject_proposal(proposal_id:, reason: nil, **)
|
|
70
|
+
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
71
|
+
return { success: false, error: :not_found } unless proposal
|
|
72
|
+
|
|
73
|
+
proposal.transition!(:rejected)
|
|
74
|
+
{ success: true, proposal_id: proposal_id, status: :rejected, reason: reason }
|
|
75
|
+
rescue ArgumentError => e
|
|
76
|
+
{ success: false, error: e.message }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def governance_stats(**)
|
|
80
|
+
all_votes = votes_mutex.synchronize { votes_store.dup }
|
|
81
|
+
|
|
82
|
+
total_votes = all_votes.values.sum(&:size)
|
|
83
|
+
proposals_with_votes = all_votes.size
|
|
84
|
+
|
|
85
|
+
vote_summary = all_votes.transform_values do |ballots|
|
|
86
|
+
{
|
|
87
|
+
approve: ballots.count { |b| b[:vote] == :approve },
|
|
88
|
+
reject: ballots.count { |b| b[:vote] == :reject },
|
|
89
|
+
total: ballots.size
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
proposal_stats = Runners::Proposer.proposal_stats
|
|
94
|
+
by_status = proposal_stats[:stats][:by_status]
|
|
95
|
+
|
|
96
|
+
governance_breakdown = Helpers::Constants::GOVERNANCE_STATUSES.to_h do |s|
|
|
97
|
+
[s, by_status[s] || 0]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
success: true,
|
|
102
|
+
total_votes: total_votes,
|
|
103
|
+
proposals_with_votes: proposals_with_votes,
|
|
104
|
+
vote_summary: vote_summary,
|
|
105
|
+
governance_breakdown: governance_breakdown
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def votes_store
|
|
112
|
+
@votes_store ||= {}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def votes_mutex
|
|
116
|
+
@votes_mutex ||= Mutex.new
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MindGrowth
|
|
6
|
+
module Runners
|
|
7
|
+
module Monitor
|
|
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
|
+
HEALTH_LEVELS = {
|
|
14
|
+
excellent: 0.8,
|
|
15
|
+
good: 0.6,
|
|
16
|
+
fair: 0.4,
|
|
17
|
+
degraded: 0.2,
|
|
18
|
+
critical: 0.0
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def health_check(extension:, **)
|
|
22
|
+
name = extension[:name] || extension[:extension_name]
|
|
23
|
+
fitness = Helpers::FitnessEvaluator.fitness(extension)
|
|
24
|
+
level = classify_health(fitness)
|
|
25
|
+
alert = %i[degraded critical].include?(level)
|
|
26
|
+
|
|
27
|
+
{ success: true, extension_name: name, fitness: fitness,
|
|
28
|
+
health_level: level, alert: alert }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def usage_stats(extensions:, **)
|
|
32
|
+
stats = Array(extensions).map do |ext|
|
|
33
|
+
{ extension_name: ext[:name] || ext[:extension_name],
|
|
34
|
+
invocation_count: ext[:invocation_count] || 0,
|
|
35
|
+
error_rate: ext[:error_rate] || 0.0,
|
|
36
|
+
avg_latency_ms: ext[:avg_latency_ms] || 0 }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
{ success: true, stats: stats, count: stats.size }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def impact_score(extension:, extensions: nil, **)
|
|
43
|
+
name = extension[:name] || extension[:extension_name]
|
|
44
|
+
impact = extension[:impact_score] || 0.5
|
|
45
|
+
|
|
46
|
+
percentile = if extensions && !Array(extensions).empty?
|
|
47
|
+
all_impacts = Array(extensions).map { |e| e[:impact_score] || 0.5 }.sort
|
|
48
|
+
rank = all_impacts.count { |i| i <= impact }
|
|
49
|
+
(rank.to_f / all_impacts.size * 100).round(1)
|
|
50
|
+
else
|
|
51
|
+
50.0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
{ success: true, extension_name: name, impact: impact, rank_percentile: percentile }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decay_check(extensions:, **)
|
|
58
|
+
threshold = Helpers::Constants::DECAY_INVOCATION_THRESHOLD
|
|
59
|
+
decayed = Array(extensions).select do |ext|
|
|
60
|
+
count = ext[:invocation_count] || 0
|
|
61
|
+
fitness = Helpers::FitnessEvaluator.fitness(ext)
|
|
62
|
+
count < threshold || fitness < Helpers::Constants::PRUNE_THRESHOLD
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{ success: true, decayed: decayed, count: decayed.size }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def auto_prune(extensions:, **)
|
|
69
|
+
pruned = Helpers::FitnessEvaluator.prune_candidates(Array(extensions))
|
|
70
|
+
{ success: true, pruned: pruned, count: pruned.size }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def health_summary(extensions:, **)
|
|
74
|
+
exts = Array(extensions)
|
|
75
|
+
|
|
76
|
+
by_health_level = HEALTH_LEVELS.keys.to_h { |level| [level, 0] }
|
|
77
|
+
alerts = []
|
|
78
|
+
prune_candidates = Helpers::FitnessEvaluator.prune_candidates(exts)
|
|
79
|
+
|
|
80
|
+
exts.each do |ext|
|
|
81
|
+
fitness = Helpers::FitnessEvaluator.fitness(ext)
|
|
82
|
+
level = classify_health(fitness)
|
|
83
|
+
by_health_level[level] += 1
|
|
84
|
+
alerts << ext if %i[degraded critical].include?(level)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{ success: true, total: exts.size, by_health_level: by_health_level,
|
|
88
|
+
alerts: alerts, prune_candidates: prune_candidates }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def classify_health(fitness)
|
|
94
|
+
HEALTH_LEVELS.each do |level, threshold|
|
|
95
|
+
return level if fitness >= threshold
|
|
96
|
+
end
|
|
97
|
+
:critical
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|