lex-mind-growth 0.1.6 → 0.1.7

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: '0855bf68cbc39002e5c08851edd6f3ce71afddb38f0633078a68d878fe954dec'
4
- data.tar.gz: dc106552129f3d41f91711c51fca3b2a90a0198aede48d14b4d589c0b319fea0
3
+ metadata.gz: 2182f87f0f59f727c61f209083c225b1b31d9cbb5589af437618350433e8b692
4
+ data.tar.gz: 1ee0428c83686efdb168054b16d68af66443dfa60bd5f0bd8e2725f4d7d87ab4
5
5
  SHA512:
6
- metadata.gz: 7d423da820af5d21f8b8f33f3c86850ae6cc6734d717386d31a862b800573694148626ade4587f5d2dbf10bafa14a61f0c973a2b1cefb1fe92817ef6c38527cd
7
- data.tar.gz: 613aabc82f554a7de59965e8bb6a6543a72e346e74ea6618a6de65c15b4b40a2d2c4220230281dbf51e613aaa87d06b40738988627137244e9c7247229aef1be
6
+ metadata.gz: 1e7320c15ecd31d531bd961b03f20baa8c30a77946f98e58c25557ebcd1027b42b7c9af7c9ce8ae781089e25966a76d183209922ce8b2e7a780c141853cf6d96
7
+ data.tar.gz: b7abcbc0ea3109d64a4cc1cda9508ea0ee8ab777271e205abc1ae4903d1f2bf349b78053aa071fe5835790a790f4974be95800cf57ad2bfa8ab96182b358d312
@@ -35,6 +35,18 @@ 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(**)
38
50
  end
39
51
  end
40
52
  end
@@ -48,6 +48,20 @@ 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
+ # Risk assessment
58
+ RISK_TIERS = %i[low medium high critical].freeze
59
+ RISK_RECOMMENDATIONS = {
60
+ low: :auto_approve,
61
+ medium: :governance,
62
+ high: :human_required,
63
+ critical: :blocked
64
+ }.freeze
51
65
  end
52
66
  end
53
67
  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,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module RiskAssessor
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
+ HIGH_BLAST_CATEGORIES = %i[safety coordination].freeze
14
+ MEDIUM_BLAST_CATEGORIES = %i[cognition].freeze
15
+ HOT_PATH_CATEGORIES = %i[perception memory].freeze
16
+
17
+ def assess_risk(proposal_id:, **)
18
+ proposal = Runners::Proposer.get_proposal_object(proposal_id)
19
+ return { success: false, error: :not_found } unless proposal
20
+
21
+ dimensions = evaluate_dimensions(proposal)
22
+ tier = calculate_tier(dimensions)
23
+ recommendation = Helpers::Constants::RISK_RECOMMENDATIONS[tier]
24
+
25
+ { success: true, proposal_id: proposal_id, risk_tier: tier,
26
+ dimensions: dimensions, recommendation: recommendation }
27
+ end
28
+
29
+ def risk_summary(proposals: nil, **)
30
+ ids = if proposals
31
+ Array(proposals).map { |p| p.is_a?(Hash) ? p[:id] : p.to_s }
32
+ else
33
+ Runners::Proposer.list_proposals(limit: 100)[:proposals].map { |p| p[:id] }
34
+ end
35
+
36
+ results = ids.filter_map do |id|
37
+ result = assess_risk(proposal_id: id)
38
+ next unless result[:success]
39
+
40
+ result
41
+ end
42
+
43
+ grouped = Helpers::Constants::RISK_TIERS.to_h { |tier| [tier, []] }
44
+ results.each { |r| grouped[r[:risk_tier]] << r }
45
+
46
+ { success: true, total: results.size, by_tier: grouped }
47
+ end
48
+
49
+ private
50
+
51
+ def evaluate_dimensions(proposal)
52
+ helper_count = Array(proposal.helpers).size
53
+ category = proposal.category.to_sym
54
+
55
+ {
56
+ complexity: complexity_level(helper_count, Array(proposal.runner_methods).size),
57
+ blast_radius: blast_radius_level(category),
58
+ reversibility: :high,
59
+ performance_impact: performance_impact_level(category)
60
+ }
61
+ end
62
+
63
+ def complexity_level(helper_count, runner_count)
64
+ total = helper_count + runner_count
65
+ if total >= 7
66
+ :high
67
+ elsif total >= 4
68
+ :medium
69
+ else
70
+ :low
71
+ end
72
+ end
73
+
74
+ def blast_radius_level(category)
75
+ if HIGH_BLAST_CATEGORIES.include?(category)
76
+ :high
77
+ elsif MEDIUM_BLAST_CATEGORIES.include?(category)
78
+ :medium
79
+ else
80
+ :low
81
+ end
82
+ end
83
+
84
+ def performance_impact_level(category)
85
+ HOT_PATH_CATEGORIES.include?(category) ? :medium : :low
86
+ end
87
+
88
+ def calculate_tier(dimensions)
89
+ # Reversibility is a positive attribute (high = easily reversed) — exclude from risk calc
90
+ risk_values = dimensions.except(:reversibility).values
91
+
92
+ if risk_values.include?(:critical)
93
+ :critical
94
+ elsif risk_values.include?(:high)
95
+ :high
96
+ elsif risk_values.include?(:medium)
97
+ :medium
98
+ else
99
+ :low
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MindGrowth
6
- VERSION = '0.1.6'
6
+ VERSION = '0.1.7'
7
7
  end
8
8
  end
9
9
  end
@@ -18,6 +18,8 @@ require 'legion/extensions/mind_growth/runners/orchestrator'
18
18
  require 'legion/extensions/mind_growth/runners/wirer'
19
19
  require 'legion/extensions/mind_growth/runners/integration_tester'
20
20
  require 'legion/extensions/mind_growth/runners/retrospective'
21
+ require 'legion/extensions/mind_growth/runners/governance'
22
+ require 'legion/extensions/mind_growth/runners/risk_assessor'
21
23
  require 'legion/extensions/mind_growth/client'
22
24
 
23
25
  module Legion
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MindGrowth::Runners::Governance do
4
+ subject(:governance) { described_class }
5
+
6
+ let(:proposer) { Legion::Extensions::MindGrowth::Runners::Proposer }
7
+
8
+ before do
9
+ proposer.instance_variable_set(:@proposal_store, nil)
10
+ governance.instance_variable_set(:@votes_store, nil)
11
+ governance.instance_variable_set(:@votes_mutex, nil)
12
+ end
13
+
14
+ def create_proposal(name: 'lex-gov-test', category: :cognition, status: nil)
15
+ result = proposer.propose_concept(name: name, category: category, description: 'test proposal', enrich: false)
16
+ proposal = proposer.get_proposal_object(result[:proposal][:id])
17
+ proposal.transition!(status) if status && status != :proposed
18
+ proposal
19
+ end
20
+
21
+ # ─── submit_proposal ──────────────────────────────────────────────────────
22
+
23
+ describe '.submit_proposal' do
24
+ context 'with a valid proposed proposal' do
25
+ it 'returns success: true' do
26
+ proposal = create_proposal
27
+ result = governance.submit_proposal(proposal_id: proposal.id)
28
+ expect(result[:success]).to be true
29
+ end
30
+
31
+ it 'returns the proposal_id' do
32
+ proposal = create_proposal
33
+ result = governance.submit_proposal(proposal_id: proposal.id)
34
+ expect(result[:proposal_id]).to eq(proposal.id)
35
+ end
36
+
37
+ it 'returns status: :evaluating' do
38
+ proposal = create_proposal
39
+ result = governance.submit_proposal(proposal_id: proposal.id)
40
+ expect(result[:status]).to eq(:evaluating)
41
+ end
42
+
43
+ it 'transitions the proposal to :evaluating' do
44
+ proposal = create_proposal
45
+ governance.submit_proposal(proposal_id: proposal.id)
46
+ expect(proposal.status).to eq(:evaluating)
47
+ end
48
+ end
49
+
50
+ context 'with an already-evaluating proposal' do
51
+ it 'returns success: true and keeps :evaluating status' do
52
+ proposal = create_proposal
53
+ governance.submit_proposal(proposal_id: proposal.id)
54
+ result = governance.submit_proposal(proposal_id: proposal.id)
55
+ expect(result[:success]).to be true
56
+ expect(result[:status]).to eq(:evaluating)
57
+ end
58
+ end
59
+
60
+ context 'with a proposal in an invalid status' do
61
+ it 'returns success: false for :building status' do
62
+ proposal = create_proposal(status: :building)
63
+ result = governance.submit_proposal(proposal_id: proposal.id)
64
+ expect(result[:success]).to be false
65
+ end
66
+
67
+ it 'returns :invalid_status error' do
68
+ proposal = create_proposal(status: :building)
69
+ result = governance.submit_proposal(proposal_id: proposal.id)
70
+ expect(result[:error]).to eq(:invalid_status)
71
+ end
72
+
73
+ it 'includes current_status in the error response' do
74
+ proposal = create_proposal(status: :building)
75
+ result = governance.submit_proposal(proposal_id: proposal.id)
76
+ expect(result[:current_status]).to eq(:building)
77
+ end
78
+ end
79
+
80
+ context 'with a non-existent proposal_id' do
81
+ it 'returns success: false' do
82
+ result = governance.submit_proposal(proposal_id: 'no-such-id')
83
+ expect(result[:success]).to be false
84
+ end
85
+
86
+ it 'returns :not_found error' do
87
+ result = governance.submit_proposal(proposal_id: 'no-such-id')
88
+ expect(result[:error]).to eq(:not_found)
89
+ end
90
+ end
91
+
92
+ it 'ignores unknown keyword arguments' do
93
+ proposal = create_proposal
94
+ expect { governance.submit_proposal(proposal_id: proposal.id, extra: true) }.not_to raise_error
95
+ end
96
+ end
97
+
98
+ # ─── vote_on_proposal ─────────────────────────────────────────────────────
99
+
100
+ describe '.vote_on_proposal' do
101
+ let(:proposal) { create_proposal }
102
+
103
+ it 'returns success: true for :approve vote' do
104
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve)
105
+ expect(result[:success]).to be true
106
+ end
107
+
108
+ it 'returns success: true for :reject vote' do
109
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject)
110
+ expect(result[:success]).to be true
111
+ end
112
+
113
+ it 'returns the proposal_id' do
114
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve)
115
+ expect(result[:proposal_id]).to eq(proposal.id)
116
+ end
117
+
118
+ it 'returns the vote symbol' do
119
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve)
120
+ expect(result[:vote]).to eq(:approve)
121
+ end
122
+
123
+ it 'returns the agent_id' do
124
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: 'agent-1')
125
+ expect(result[:agent_id]).to eq('agent-1')
126
+ end
127
+
128
+ it 'uses "default" as agent_id when not provided' do
129
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve)
130
+ expect(result[:agent_id]).to eq('default')
131
+ end
132
+
133
+ it 'accepts string vote values and coerces to symbol' do
134
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: 'approve')
135
+ expect(result[:success]).to be true
136
+ expect(result[:vote]).to eq(:approve)
137
+ end
138
+
139
+ it 'returns success: false for invalid vote' do
140
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :maybe)
141
+ expect(result[:success]).to be false
142
+ end
143
+
144
+ it 'returns :invalid_vote error for unknown vote' do
145
+ result = governance.vote_on_proposal(proposal_id: proposal.id, vote: :abstain)
146
+ expect(result[:error]).to eq(:invalid_vote)
147
+ end
148
+
149
+ it 'accumulates multiple votes for the same proposal' do
150
+ 3.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "agent-#{i}") }
151
+ tally = governance.tally_votes(proposal_id: proposal.id)
152
+ expect(tally[:total]).to eq(3)
153
+ end
154
+
155
+ context 'thread safety' do
156
+ it 'records all votes when cast concurrently' do
157
+ threads = 10.times.map do |i|
158
+ Thread.new { governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "t#{i}") }
159
+ end
160
+ threads.each(&:join)
161
+ tally = governance.tally_votes(proposal_id: proposal.id)
162
+ expect(tally[:total]).to eq(10)
163
+ end
164
+
165
+ it 'correctly tallies mixed concurrent votes' do
166
+ threads = []
167
+ 5.times { |i| threads << Thread.new { governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") } }
168
+ 5.times { |i| threads << Thread.new { governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject, agent_id: "r#{i}") } }
169
+ threads.each(&:join)
170
+ tally = governance.tally_votes(proposal_id: proposal.id)
171
+ expect(tally[:approve_count]).to eq(5)
172
+ expect(tally[:reject_count]).to eq(5)
173
+ end
174
+ end
175
+ end
176
+
177
+ # ─── tally_votes ──────────────────────────────────────────────────────────
178
+
179
+ describe '.tally_votes' do
180
+ let(:proposal) { create_proposal }
181
+
182
+ it 'returns success: true' do
183
+ result = governance.tally_votes(proposal_id: proposal.id)
184
+ expect(result[:success]).to be true
185
+ end
186
+
187
+ it 'returns the proposal_id' do
188
+ result = governance.tally_votes(proposal_id: proposal.id)
189
+ expect(result[:proposal_id]).to eq(proposal.id)
190
+ end
191
+
192
+ it 'returns zero counts with no votes' do
193
+ result = governance.tally_votes(proposal_id: proposal.id)
194
+ expect(result[:approve_count]).to eq(0)
195
+ expect(result[:reject_count]).to eq(0)
196
+ expect(result[:total]).to eq(0)
197
+ end
198
+
199
+ it 'returns :pending verdict when total < QUORUM' do
200
+ 2.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") }
201
+ result = governance.tally_votes(proposal_id: proposal.id)
202
+ expect(result[:verdict]).to eq(:pending)
203
+ end
204
+
205
+ it 'returns :approved verdict when approve_count > reject_count and total >= QUORUM' do
206
+ 3.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") }
207
+ result = governance.tally_votes(proposal_id: proposal.id)
208
+ expect(result[:verdict]).to eq(:approved)
209
+ end
210
+
211
+ it 'returns :rejected verdict when reject_count >= approve_count and total >= QUORUM' do
212
+ 2.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject, agent_id: "r#{i}") }
213
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: 'a0')
214
+ result = governance.tally_votes(proposal_id: proposal.id)
215
+ expect(result[:verdict]).to eq(:rejected)
216
+ end
217
+
218
+ it 'returns :rejected on a tie when total >= QUORUM' do
219
+ 3.times do |i|
220
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}")
221
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject, agent_id: "r#{i}")
222
+ end
223
+ result = governance.tally_votes(proposal_id: proposal.id)
224
+ expect(result[:verdict]).to eq(:rejected)
225
+ end
226
+
227
+ it 'counts approve votes correctly' do
228
+ 2.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") }
229
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject, agent_id: 'r0')
230
+ result = governance.tally_votes(proposal_id: proposal.id)
231
+ expect(result[:approve_count]).to eq(2)
232
+ expect(result[:reject_count]).to eq(1)
233
+ end
234
+
235
+ it 'returns :pending for an unknown proposal_id with no votes' do
236
+ result = governance.tally_votes(proposal_id: 'nonexistent')
237
+ expect(result[:verdict]).to eq(:pending)
238
+ expect(result[:total]).to eq(0)
239
+ end
240
+
241
+ it 'requires exactly QUORUM votes for non-pending verdict' do
242
+ quorum = Legion::Extensions::MindGrowth::Helpers::Constants::QUORUM
243
+ (quorum - 1).times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") }
244
+ result = governance.tally_votes(proposal_id: proposal.id)
245
+ expect(result[:verdict]).to eq(:pending)
246
+
247
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: 'final')
248
+ result = governance.tally_votes(proposal_id: proposal.id)
249
+ expect(result[:verdict]).to eq(:approved)
250
+ end
251
+ end
252
+
253
+ # ─── approve_proposal ─────────────────────────────────────────────────────
254
+
255
+ describe '.approve_proposal' do
256
+ it 'returns success: true' do
257
+ proposal = create_proposal
258
+ result = governance.approve_proposal(proposal_id: proposal.id)
259
+ expect(result[:success]).to be true
260
+ end
261
+
262
+ it 'returns status: :approved' do
263
+ proposal = create_proposal
264
+ result = governance.approve_proposal(proposal_id: proposal.id)
265
+ expect(result[:status]).to eq(:approved)
266
+ end
267
+
268
+ it 'transitions the proposal to :approved' do
269
+ proposal = create_proposal
270
+ governance.approve_proposal(proposal_id: proposal.id)
271
+ expect(proposal.status).to eq(:approved)
272
+ end
273
+
274
+ it 'returns the proposal_id' do
275
+ proposal = create_proposal
276
+ result = governance.approve_proposal(proposal_id: proposal.id)
277
+ expect(result[:proposal_id]).to eq(proposal.id)
278
+ end
279
+
280
+ it 'returns success: false for non-existent proposal' do
281
+ result = governance.approve_proposal(proposal_id: 'missing')
282
+ expect(result[:success]).to be false
283
+ expect(result[:error]).to eq(:not_found)
284
+ end
285
+
286
+ it 'accepts an optional reason keyword' do
287
+ proposal = create_proposal
288
+ expect { governance.approve_proposal(proposal_id: proposal.id, reason: 'looks good') }.not_to raise_error
289
+ end
290
+ end
291
+
292
+ # ─── reject_proposal ──────────────────────────────────────────────────────
293
+
294
+ describe '.reject_proposal' do
295
+ it 'returns success: true' do
296
+ proposal = create_proposal
297
+ result = governance.reject_proposal(proposal_id: proposal.id)
298
+ expect(result[:success]).to be true
299
+ end
300
+
301
+ it 'returns status: :rejected' do
302
+ proposal = create_proposal
303
+ result = governance.reject_proposal(proposal_id: proposal.id)
304
+ expect(result[:status]).to eq(:rejected)
305
+ end
306
+
307
+ it 'transitions the proposal to :rejected' do
308
+ proposal = create_proposal
309
+ governance.reject_proposal(proposal_id: proposal.id)
310
+ expect(proposal.status).to eq(:rejected)
311
+ end
312
+
313
+ it 'returns the proposal_id' do
314
+ proposal = create_proposal
315
+ result = governance.reject_proposal(proposal_id: proposal.id)
316
+ expect(result[:proposal_id]).to eq(proposal.id)
317
+ end
318
+
319
+ it 'includes the reason in the response' do
320
+ proposal = create_proposal
321
+ result = governance.reject_proposal(proposal_id: proposal.id, reason: 'too risky')
322
+ expect(result[:reason]).to eq('too risky')
323
+ end
324
+
325
+ it 'returns nil reason when none provided' do
326
+ proposal = create_proposal
327
+ result = governance.reject_proposal(proposal_id: proposal.id)
328
+ expect(result[:reason]).to be_nil
329
+ end
330
+
331
+ it 'returns success: false for non-existent proposal' do
332
+ result = governance.reject_proposal(proposal_id: 'missing')
333
+ expect(result[:success]).to be false
334
+ expect(result[:error]).to eq(:not_found)
335
+ end
336
+ end
337
+
338
+ # ─── governance_stats ─────────────────────────────────────────────────────
339
+
340
+ describe '.governance_stats' do
341
+ it 'returns success: true' do
342
+ result = governance.governance_stats
343
+ expect(result[:success]).to be true
344
+ end
345
+
346
+ it 'includes total_votes key' do
347
+ result = governance.governance_stats
348
+ expect(result).to have_key(:total_votes)
349
+ end
350
+
351
+ it 'includes proposals_with_votes key' do
352
+ result = governance.governance_stats
353
+ expect(result).to have_key(:proposals_with_votes)
354
+ end
355
+
356
+ it 'includes vote_summary key' do
357
+ result = governance.governance_stats
358
+ expect(result).to have_key(:vote_summary)
359
+ end
360
+
361
+ it 'includes governance_breakdown key' do
362
+ result = governance.governance_stats
363
+ expect(result).to have_key(:governance_breakdown)
364
+ end
365
+
366
+ it 'reports zero total_votes when no votes cast' do
367
+ result = governance.governance_stats
368
+ expect(result[:total_votes]).to eq(0)
369
+ end
370
+
371
+ it 'reports zero proposals_with_votes when no votes cast' do
372
+ result = governance.governance_stats
373
+ expect(result[:proposals_with_votes]).to eq(0)
374
+ end
375
+
376
+ it 'governance_breakdown includes all GOVERNANCE_STATUSES' do
377
+ result = governance.governance_stats
378
+ Legion::Extensions::MindGrowth::Helpers::Constants::GOVERNANCE_STATUSES.each do |status|
379
+ expect(result[:governance_breakdown]).to have_key(status)
380
+ end
381
+ end
382
+
383
+ context 'after voting' do
384
+ let(:proposal) { create_proposal }
385
+
386
+ before do
387
+ 3.times { |i| governance.vote_on_proposal(proposal_id: proposal.id, vote: :approve, agent_id: "a#{i}") }
388
+ governance.vote_on_proposal(proposal_id: proposal.id, vote: :reject, agent_id: 'r0')
389
+ end
390
+
391
+ it 'reflects total_votes correctly' do
392
+ result = governance.governance_stats
393
+ expect(result[:total_votes]).to eq(4)
394
+ end
395
+
396
+ it 'reflects proposals_with_votes correctly' do
397
+ result = governance.governance_stats
398
+ expect(result[:proposals_with_votes]).to eq(1)
399
+ end
400
+
401
+ it 'vote_summary for the proposal has correct approve/reject counts' do
402
+ result = governance.governance_stats
403
+ summary = result[:vote_summary][proposal.id]
404
+ expect(summary[:approve]).to eq(3)
405
+ expect(summary[:reject]).to eq(1)
406
+ expect(summary[:total]).to eq(4)
407
+ end
408
+ end
409
+
410
+ context 'with multiple proposals' do
411
+ it 'counts votes across proposals correctly' do
412
+ p1 = create_proposal(name: 'lex-g1')
413
+ p2 = create_proposal(name: 'lex-g2')
414
+ 2.times { |i| governance.vote_on_proposal(proposal_id: p1.id, vote: :approve, agent_id: "a#{i}") }
415
+ 3.times { |i| governance.vote_on_proposal(proposal_id: p2.id, vote: :reject, agent_id: "r#{i}") }
416
+ result = governance.governance_stats
417
+ expect(result[:total_votes]).to eq(5)
418
+ expect(result[:proposals_with_votes]).to eq(2)
419
+ end
420
+ end
421
+ end
422
+
423
+ # ─── constant checks ──────────────────────────────────────────────────────
424
+
425
+ describe 'constants' do
426
+ it 'QUORUM is 3' do
427
+ expect(Legion::Extensions::MindGrowth::Helpers::Constants::QUORUM).to eq(3)
428
+ end
429
+
430
+ it 'REJECTION_COOLDOWN_HOURS is 24' do
431
+ expect(Legion::Extensions::MindGrowth::Helpers::Constants::REJECTION_COOLDOWN_HOURS).to eq(24)
432
+ end
433
+
434
+ it 'GOVERNANCE_STATUSES includes :pending, :approved, :rejected, :expired' do
435
+ statuses = Legion::Extensions::MindGrowth::Helpers::Constants::GOVERNANCE_STATUSES
436
+ expect(statuses).to include(:pending, :approved, :rejected, :expired)
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,395 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MindGrowth::Runners::RiskAssessor do
4
+ subject(:risk_assessor) { described_class }
5
+
6
+ let(:proposer) { Legion::Extensions::MindGrowth::Runners::Proposer }
7
+
8
+ before { proposer.instance_variable_set(:@proposal_store, nil) }
9
+
10
+ def create_proposal(name: 'lex-risk-test', category: :cognition, helpers: [], runner_methods: [])
11
+ result = proposer.propose_concept(name: name, category: category, description: 'risk test', enrich: false)
12
+ proposal = proposer.get_proposal_object(result[:proposal][:id])
13
+ # Inject helpers and runner_methods via instance variables for test control
14
+ proposal.instance_variable_set(:@helpers, helpers)
15
+ proposal.instance_variable_set(:@runner_methods, runner_methods)
16
+ proposal
17
+ end
18
+
19
+ # ─── assess_risk ──────────────────────────────────────────────────────────
20
+
21
+ describe '.assess_risk' do
22
+ context 'with a basic cognition proposal' do
23
+ let(:proposal) { create_proposal }
24
+
25
+ it 'returns success: true' do
26
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
27
+ expect(result[:success]).to be true
28
+ end
29
+
30
+ it 'returns the proposal_id' do
31
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
32
+ expect(result[:proposal_id]).to eq(proposal.id)
33
+ end
34
+
35
+ it 'returns a risk_tier' do
36
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
37
+ expect(Legion::Extensions::MindGrowth::Helpers::Constants::RISK_TIERS).to include(result[:risk_tier])
38
+ end
39
+
40
+ it 'returns a dimensions hash' do
41
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
42
+ expect(result[:dimensions]).to be_a(Hash)
43
+ end
44
+
45
+ it 'dimensions includes :complexity' do
46
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
47
+ expect(result[:dimensions]).to have_key(:complexity)
48
+ end
49
+
50
+ it 'dimensions includes :blast_radius' do
51
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
52
+ expect(result[:dimensions]).to have_key(:blast_radius)
53
+ end
54
+
55
+ it 'dimensions includes :reversibility' do
56
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
57
+ expect(result[:dimensions]).to have_key(:reversibility)
58
+ end
59
+
60
+ it 'dimensions includes :performance_impact' do
61
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
62
+ expect(result[:dimensions]).to have_key(:performance_impact)
63
+ end
64
+
65
+ it 'reversibility is always :high' do
66
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
67
+ expect(result[:dimensions][:reversibility]).to eq(:high)
68
+ end
69
+
70
+ it 'returns a recommendation' do
71
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
72
+ expect(Legion::Extensions::MindGrowth::Helpers::Constants::RISK_RECOMMENDATIONS.values).to include(result[:recommendation])
73
+ end
74
+ end
75
+
76
+ context 'with a non-existent proposal_id' do
77
+ it 'returns success: false' do
78
+ result = risk_assessor.assess_risk(proposal_id: 'no-such-id')
79
+ expect(result[:success]).to be false
80
+ end
81
+
82
+ it 'returns :not_found error' do
83
+ result = risk_assessor.assess_risk(proposal_id: 'no-such-id')
84
+ expect(result[:error]).to eq(:not_found)
85
+ end
86
+ end
87
+
88
+ it 'ignores unknown keyword arguments' do
89
+ proposal = create_proposal
90
+ expect { risk_assessor.assess_risk(proposal_id: proposal.id, extra: true) }.not_to raise_error
91
+ end
92
+
93
+ # ── complexity dimension ──────────────────────────────────────────────
94
+
95
+ context 'complexity based on helpers + runner_methods count' do
96
+ it 'is :low when total < 4 (0 helpers, 0 runner_methods)' do
97
+ proposal = create_proposal(helpers: [], runner_methods: [])
98
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
99
+ expect(result[:dimensions][:complexity]).to eq(:low)
100
+ end
101
+
102
+ it 'is :low when total is 3' do
103
+ proposal = create_proposal(helpers: [{ name: 'h1' }, { name: 'h2' }], runner_methods: [{ name: 'r1' }])
104
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
105
+ expect(result[:dimensions][:complexity]).to eq(:low)
106
+ end
107
+
108
+ it 'is :medium when total is 4' do
109
+ helpers = Array.new(2) { |i| { name: "h#{i}" } }
110
+ runners = Array.new(2) { |i| { name: "r#{i}" } }
111
+ proposal = create_proposal(helpers: helpers, runner_methods: runners)
112
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
113
+ expect(result[:dimensions][:complexity]).to eq(:medium)
114
+ end
115
+
116
+ it 'is :medium when total is 6' do
117
+ helpers = Array.new(3) { |i| { name: "h#{i}" } }
118
+ runners = Array.new(3) { |i| { name: "r#{i}" } }
119
+ proposal = create_proposal(helpers: helpers, runner_methods: runners)
120
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
121
+ expect(result[:dimensions][:complexity]).to eq(:medium)
122
+ end
123
+
124
+ it 'is :high when total is 7' do
125
+ helpers = Array.new(4) { |i| { name: "h#{i}" } }
126
+ runners = Array.new(3) { |i| { name: "r#{i}" } }
127
+ proposal = create_proposal(helpers: helpers, runner_methods: runners)
128
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
129
+ expect(result[:dimensions][:complexity]).to eq(:high)
130
+ end
131
+
132
+ it 'is :high when total > 7' do
133
+ helpers = Array.new(5) { |i| { name: "h#{i}" } }
134
+ runners = Array.new(5) { |i| { name: "r#{i}" } }
135
+ proposal = create_proposal(helpers: helpers, runner_methods: runners)
136
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
137
+ expect(result[:dimensions][:complexity]).to eq(:high)
138
+ end
139
+ end
140
+
141
+ # ── blast_radius dimension ────────────────────────────────────────────
142
+
143
+ context 'blast_radius based on category' do
144
+ it 'is :high for :safety category' do
145
+ proposal = create_proposal(category: :safety)
146
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
147
+ expect(result[:dimensions][:blast_radius]).to eq(:high)
148
+ end
149
+
150
+ it 'is :high for :coordination category' do
151
+ proposal = create_proposal(name: 'lex-coord', category: :coordination)
152
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
153
+ expect(result[:dimensions][:blast_radius]).to eq(:high)
154
+ end
155
+
156
+ it 'is :medium for :cognition category' do
157
+ proposal = create_proposal(category: :cognition)
158
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
159
+ expect(result[:dimensions][:blast_radius]).to eq(:medium)
160
+ end
161
+
162
+ it 'is :low for :communication category' do
163
+ proposal = create_proposal(name: 'lex-comm', category: :communication)
164
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
165
+ expect(result[:dimensions][:blast_radius]).to eq(:low)
166
+ end
167
+
168
+ it 'is :low for :motivation category' do
169
+ proposal = create_proposal(name: 'lex-motiv', category: :motivation)
170
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
171
+ expect(result[:dimensions][:blast_radius]).to eq(:low)
172
+ end
173
+
174
+ it 'is :low for :introspection category' do
175
+ proposal = create_proposal(name: 'lex-intro', category: :introspection)
176
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
177
+ expect(result[:dimensions][:blast_radius]).to eq(:low)
178
+ end
179
+ end
180
+
181
+ # ── performance_impact dimension ──────────────────────────────────────
182
+
183
+ context 'performance_impact based on category' do
184
+ it 'is :medium for :perception category (hot path)' do
185
+ proposal = create_proposal(name: 'lex-perc', category: :perception)
186
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
187
+ expect(result[:dimensions][:performance_impact]).to eq(:medium)
188
+ end
189
+
190
+ it 'is :medium for :memory category (hot path)' do
191
+ proposal = create_proposal(name: 'lex-mem', category: :memory)
192
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
193
+ expect(result[:dimensions][:performance_impact]).to eq(:medium)
194
+ end
195
+
196
+ it 'is :low for :cognition category' do
197
+ proposal = create_proposal(category: :cognition)
198
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
199
+ expect(result[:dimensions][:performance_impact]).to eq(:low)
200
+ end
201
+
202
+ it 'is :low for :safety category' do
203
+ proposal = create_proposal(category: :safety)
204
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
205
+ expect(result[:dimensions][:performance_impact]).to eq(:low)
206
+ end
207
+ end
208
+
209
+ # ── risk_tier calculation ─────────────────────────────────────────────
210
+
211
+ context 'risk tier calculation' do
212
+ it 'is :low when all dimensions are low (communication, few helpers)' do
213
+ proposal = create_proposal(name: 'lex-low', category: :communication,
214
+ helpers: [], runner_methods: [])
215
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
216
+ expect(result[:risk_tier]).to eq(:low)
217
+ expect(result[:recommendation]).to eq(:auto_approve)
218
+ end
219
+
220
+ it 'is :medium when any dimension is medium (cognition with low complexity)' do
221
+ proposal = create_proposal(category: :cognition, helpers: [], runner_methods: [])
222
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
223
+ # cognition => blast_radius :medium
224
+ expect(result[:risk_tier]).to eq(:medium)
225
+ expect(result[:recommendation]).to eq(:governance)
226
+ end
227
+
228
+ it 'is :medium when perception category with low complexity' do
229
+ proposal = create_proposal(name: 'lex-perc2', category: :perception,
230
+ helpers: [], runner_methods: [])
231
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
232
+ # perception => performance_impact :medium
233
+ expect(result[:risk_tier]).to eq(:medium)
234
+ end
235
+
236
+ it 'is :high when blast_radius is :high (safety category)' do
237
+ proposal = create_proposal(category: :safety)
238
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
239
+ expect(result[:risk_tier]).to eq(:high)
240
+ expect(result[:recommendation]).to eq(:human_required)
241
+ end
242
+
243
+ it 'is :high when blast_radius is :high (coordination category)' do
244
+ proposal = create_proposal(name: 'lex-coord2', category: :coordination)
245
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
246
+ expect(result[:risk_tier]).to eq(:high)
247
+ end
248
+
249
+ it 'is :high when complexity is :high (7+ helpers+runners)' do
250
+ helpers = Array.new(4) { |i| { name: "h#{i}" } }
251
+ runners = Array.new(4) { |i| { name: "r#{i}" } }
252
+ proposal = create_proposal(name: 'lex-complex', category: :communication,
253
+ helpers: helpers, runner_methods: runners)
254
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
255
+ expect(result[:risk_tier]).to eq(:high)
256
+ end
257
+
258
+ it 'is :high (not :medium) when both blast_radius and complexity are high' do
259
+ helpers = Array.new(4) { |i| { name: "h#{i}" } }
260
+ runners = Array.new(4) { |i| { name: "r#{i}" } }
261
+ proposal = create_proposal(name: 'lex-worst', category: :safety,
262
+ helpers: helpers, runner_methods: runners)
263
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
264
+ expect(result[:risk_tier]).to eq(:high)
265
+ end
266
+ end
267
+
268
+ # ── recommendation mapping ────────────────────────────────────────────
269
+
270
+ context 'recommendation mapping' do
271
+ it 'maps :low tier to :auto_approve' do
272
+ proposal = create_proposal(name: 'lex-auto', category: :communication,
273
+ helpers: [], runner_methods: [])
274
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
275
+ expect(result[:recommendation]).to eq(:auto_approve)
276
+ end
277
+
278
+ it 'maps :medium tier to :governance' do
279
+ proposal = create_proposal(category: :cognition)
280
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
281
+ expect(result[:recommendation]).to eq(:governance)
282
+ end
283
+
284
+ it 'maps :high tier to :human_required' do
285
+ proposal = create_proposal(category: :safety)
286
+ result = risk_assessor.assess_risk(proposal_id: proposal.id)
287
+ expect(result[:recommendation]).to eq(:human_required)
288
+ end
289
+ end
290
+ end
291
+
292
+ # ─── risk_summary ─────────────────────────────────────────────────────────
293
+
294
+ describe '.risk_summary' do
295
+ before { proposer.instance_variable_set(:@proposal_store, nil) }
296
+
297
+ it 'returns success: true' do
298
+ result = risk_assessor.risk_summary
299
+ expect(result[:success]).to be true
300
+ end
301
+
302
+ it 'includes total count' do
303
+ result = risk_assessor.risk_summary
304
+ expect(result).to have_key(:total)
305
+ end
306
+
307
+ it 'includes by_tier hash' do
308
+ result = risk_assessor.risk_summary
309
+ expect(result[:by_tier]).to be_a(Hash)
310
+ end
311
+
312
+ it 'by_tier includes all risk tiers' do
313
+ result = risk_assessor.risk_summary
314
+ Legion::Extensions::MindGrowth::Helpers::Constants::RISK_TIERS.each do |tier|
315
+ expect(result[:by_tier]).to have_key(tier)
316
+ end
317
+ end
318
+
319
+ it 'returns total 0 when no proposals exist' do
320
+ result = risk_assessor.risk_summary
321
+ expect(result[:total]).to eq(0)
322
+ end
323
+
324
+ it 'all tier arrays are empty when no proposals' do
325
+ result = risk_assessor.risk_summary
326
+ result[:by_tier].each_value { |arr| expect(arr).to eq([]) }
327
+ end
328
+
329
+ context 'with proposals in the store' do
330
+ before do
331
+ create_proposal(name: 'lex-rs1', category: :communication, helpers: [], runner_methods: [])
332
+ create_proposal(name: 'lex-rs2', category: :safety, helpers: [], runner_methods: [])
333
+ create_proposal(name: 'lex-rs3', category: :cognition, helpers: [], runner_methods: [])
334
+ end
335
+
336
+ it 'counts total proposals assessed' do
337
+ result = risk_assessor.risk_summary
338
+ expect(result[:total]).to eq(3)
339
+ end
340
+
341
+ it 'places low-risk proposal in :low tier' do
342
+ result = risk_assessor.risk_summary
343
+ low_names = result[:by_tier][:low].map { |r| r[:proposal_id] }
344
+ proposal = proposer.list_proposals[:proposals].find { |p| p[:name] == 'lex-rs1' }
345
+ expect(low_names).to include(proposal[:id])
346
+ end
347
+
348
+ it 'places high-risk proposal in :high tier' do
349
+ result = risk_assessor.risk_summary
350
+ expect(result[:by_tier][:high].size).to eq(1)
351
+ end
352
+
353
+ it 'places medium-risk proposal in :medium tier' do
354
+ result = risk_assessor.risk_summary
355
+ expect(result[:by_tier][:medium].size).to eq(1)
356
+ end
357
+ end
358
+
359
+ context 'with explicit proposals array' do
360
+ it 'accepts proposals as array of hashes with :id keys' do
361
+ proposal = create_proposal(name: 'lex-explicit', category: :communication)
362
+ result = risk_assessor.risk_summary(proposals: [{ id: proposal.id }])
363
+ expect(result[:total]).to eq(1)
364
+ end
365
+
366
+ it 'accepts proposals as array of plain id strings' do
367
+ proposal = create_proposal(name: 'lex-str', category: :communication)
368
+ result = risk_assessor.risk_summary(proposals: [proposal.id])
369
+ expect(result[:total]).to eq(1)
370
+ end
371
+
372
+ it 'skips unknown ids gracefully' do
373
+ result = risk_assessor.risk_summary(proposals: ['nonexistent-id'])
374
+ expect(result[:total]).to eq(0)
375
+ end
376
+ end
377
+ end
378
+
379
+ # ─── constant checks ──────────────────────────────────────────────────────
380
+
381
+ describe 'constants' do
382
+ it 'RISK_TIERS contains :low, :medium, :high, :critical' do
383
+ expect(Legion::Extensions::MindGrowth::Helpers::Constants::RISK_TIERS)
384
+ .to contain_exactly(:low, :medium, :high, :critical)
385
+ end
386
+
387
+ it 'RISK_RECOMMENDATIONS maps all tiers' do
388
+ recs = Legion::Extensions::MindGrowth::Helpers::Constants::RISK_RECOMMENDATIONS
389
+ expect(recs[:low]).to eq(:auto_approve)
390
+ expect(recs[:medium]).to eq(:governance)
391
+ expect(recs[:high]).to eq(:human_required)
392
+ expect(recs[:critical]).to eq(:blocked)
393
+ end
394
+ end
395
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mind-growth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -143,10 +143,12 @@ files:
143
143
  - lib/legion/extensions/mind_growth/helpers/proposal_store.rb
144
144
  - lib/legion/extensions/mind_growth/runners/analyzer.rb
145
145
  - lib/legion/extensions/mind_growth/runners/builder.rb
146
+ - lib/legion/extensions/mind_growth/runners/governance.rb
146
147
  - lib/legion/extensions/mind_growth/runners/integration_tester.rb
147
148
  - lib/legion/extensions/mind_growth/runners/orchestrator.rb
148
149
  - lib/legion/extensions/mind_growth/runners/proposer.rb
149
150
  - lib/legion/extensions/mind_growth/runners/retrospective.rb
151
+ - lib/legion/extensions/mind_growth/runners/risk_assessor.rb
150
152
  - lib/legion/extensions/mind_growth/runners/validator.rb
151
153
  - lib/legion/extensions/mind_growth/runners/wirer.rb
152
154
  - lib/legion/extensions/mind_growth/version.rb
@@ -160,10 +162,12 @@ files:
160
162
  - spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb
161
163
  - spec/legion/extensions/mind_growth/runners/analyzer_spec.rb
162
164
  - spec/legion/extensions/mind_growth/runners/builder_spec.rb
165
+ - spec/legion/extensions/mind_growth/runners/governance_spec.rb
163
166
  - spec/legion/extensions/mind_growth/runners/integration_tester_spec.rb
164
167
  - spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb
165
168
  - spec/legion/extensions/mind_growth/runners/proposer_spec.rb
166
169
  - spec/legion/extensions/mind_growth/runners/retrospective_spec.rb
170
+ - spec/legion/extensions/mind_growth/runners/risk_assessor_spec.rb
167
171
  - spec/legion/extensions/mind_growth/runners/validator_spec.rb
168
172
  - spec/legion/extensions/mind_growth/runners/wirer_spec.rb
169
173
  - spec/spec_helper.rb