lex-governance 0.1.1
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 +7 -0
- data/Gemfile +10 -0
- data/lex-governance.gemspec +29 -0
- data/lib/legion/extensions/governance/actors/vote_timeout.rb +41 -0
- data/lib/legion/extensions/governance/client.rb +23 -0
- data/lib/legion/extensions/governance/helpers/layers.rb +36 -0
- data/lib/legion/extensions/governance/helpers/proposal.rb +90 -0
- data/lib/legion/extensions/governance/runners/governance.rb +83 -0
- data/lib/legion/extensions/governance/version.rb +9 -0
- data/lib/legion/extensions/governance.rb +14 -0
- data/spec/legion/extensions/governance/actors/vote_timeout_spec.rb +45 -0
- data/spec/legion/extensions/governance/client_spec.rb +14 -0
- data/spec/legion/extensions/governance/helpers/layers_spec.rb +190 -0
- data/spec/legion/extensions/governance/helpers/proposal_spec.rb +188 -0
- data/spec/legion/extensions/governance/runners/governance_spec.rb +101 -0
- data/spec/spec_helper.rb +20 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 872e980260f49400124a887b0a30dc0acb5a763440d17b20afc7c837a1e0ce4d
|
|
4
|
+
data.tar.gz: d6912d4151a41cac307e1e828733306b7ea1f32d85c126b13f2b8b5ac2fe4cb7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c6bf0e53ee3d3b6de474c5c3ff4f5d4d0dfc41dfa2ccf288af43e94056937ba4d4202426caf6ad24a8258fef0a330ef96e13f1b4db2935e82f367b9d03d4e0f
|
|
7
|
+
data.tar.gz: a62fda0ef8c6434646bed44760708f28bc8826c92a3e0a4572b2a68693c5ceac23defcd3b4f43b529ccb2050eb44a67ec8a65d9f0cfb16172ff5f347810ff66d
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/governance/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-governance'
|
|
7
|
+
spec.version = Legion::Extensions::Governance::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Governance'
|
|
12
|
+
spec.description = 'Four-layer distributed governance protocol for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-governance'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-governance'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-governance'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-governance'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-governance/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-governance.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Governance
|
|
8
|
+
module Actor
|
|
9
|
+
class VoteTimeout < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Governance::Runners::Governance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'timeout_proposals'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
300
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/helpers/layers'
|
|
4
|
+
require 'legion/extensions/governance/helpers/proposal'
|
|
5
|
+
require 'legion/extensions/governance/runners/governance'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Governance
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::Governance
|
|
12
|
+
|
|
13
|
+
def initialize(**)
|
|
14
|
+
@proposal_store = Helpers::Proposal.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :proposal_store
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Governance
|
|
6
|
+
module Helpers
|
|
7
|
+
module Layers
|
|
8
|
+
# Four governance layers (spec: governance-protocol-spec.md)
|
|
9
|
+
GOVERNANCE_LAYERS = %i[agent_validation anomaly_detection human_deliberation transparency].freeze
|
|
10
|
+
|
|
11
|
+
# Council quorum requirements
|
|
12
|
+
MIN_COUNCIL_SIZE = 3
|
|
13
|
+
QUORUM_FRACTION = 0.66
|
|
14
|
+
VOTE_TIMEOUT = 86_400 # 24 hours
|
|
15
|
+
PROPOSAL_CATEGORIES = %i[policy_change resource_allocation access_control emergency protocol_update].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def valid_layer?(layer)
|
|
20
|
+
GOVERNANCE_LAYERS.include?(layer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_category?(category)
|
|
24
|
+
PROPOSAL_CATEGORIES.include?(category)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def quorum_met?(votes, council_size)
|
|
28
|
+
return false if council_size < MIN_COUNCIL_SIZE
|
|
29
|
+
|
|
30
|
+
votes >= (council_size * QUORUM_FRACTION).ceil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Governance
|
|
8
|
+
module Helpers
|
|
9
|
+
class Proposal
|
|
10
|
+
attr_reader :proposals
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@proposals = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create(category:, description:, proposer:, council_size: Layers::MIN_COUNCIL_SIZE)
|
|
17
|
+
id = SecureRandom.uuid
|
|
18
|
+
@proposals[id] = {
|
|
19
|
+
proposal_id: id,
|
|
20
|
+
category: category,
|
|
21
|
+
description: description,
|
|
22
|
+
proposer: proposer,
|
|
23
|
+
council_size: council_size,
|
|
24
|
+
votes_for: [],
|
|
25
|
+
votes_against: [],
|
|
26
|
+
status: :open,
|
|
27
|
+
created_at: Time.now.utc,
|
|
28
|
+
resolved_at: nil
|
|
29
|
+
}
|
|
30
|
+
id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def vote(proposal_id, voter:, approve:)
|
|
34
|
+
prop = @proposals[proposal_id]
|
|
35
|
+
return nil unless prop && prop[:status] == :open
|
|
36
|
+
|
|
37
|
+
# Prevent double-voting
|
|
38
|
+
all_voters = prop[:votes_for] + prop[:votes_against]
|
|
39
|
+
return :already_voted if all_voters.include?(voter)
|
|
40
|
+
|
|
41
|
+
if approve
|
|
42
|
+
prop[:votes_for] << voter
|
|
43
|
+
else
|
|
44
|
+
prop[:votes_against] << voter
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
check_resolution(proposal_id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(proposal_id)
|
|
51
|
+
@proposals[proposal_id]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def open_proposals
|
|
55
|
+
@proposals.values.select { |p| p[:status] == :open }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_timed_out(proposal_id)
|
|
59
|
+
prop = @proposals[proposal_id]
|
|
60
|
+
return nil unless prop && prop[:status] == :open
|
|
61
|
+
|
|
62
|
+
prop[:status] = :timed_out
|
|
63
|
+
prop[:resolved_at] = Time.now.utc
|
|
64
|
+
prop
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def check_resolution(proposal_id)
|
|
70
|
+
prop = @proposals[proposal_id]
|
|
71
|
+
total_votes = prop[:votes_for].size + prop[:votes_against].size
|
|
72
|
+
|
|
73
|
+
if Layers.quorum_met?(prop[:votes_for].size, prop[:council_size])
|
|
74
|
+
prop[:status] = :approved
|
|
75
|
+
prop[:resolved_at] = Time.now.utc
|
|
76
|
+
:approved
|
|
77
|
+
elsif Layers.quorum_met?(prop[:votes_against].size, prop[:council_size]) ||
|
|
78
|
+
total_votes >= prop[:council_size]
|
|
79
|
+
prop[:status] = :rejected
|
|
80
|
+
prop[:resolved_at] = Time.now.utc
|
|
81
|
+
:rejected
|
|
82
|
+
else
|
|
83
|
+
:pending
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Governance
|
|
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
|
+
def create_proposal(category:, description:, proposer:, council_size: nil, **)
|
|
12
|
+
return { error: :invalid_category, valid: Helpers::Layers::PROPOSAL_CATEGORIES } unless Helpers::Layers.valid_category?(category)
|
|
13
|
+
|
|
14
|
+
size = council_size || Helpers::Layers::MIN_COUNCIL_SIZE
|
|
15
|
+
id = proposal_store.create(category: category, description: description,
|
|
16
|
+
proposer: proposer, council_size: size)
|
|
17
|
+
Legion::Logging.info "[governance] proposal created: id=#{id[0..7]} category=#{category} proposer=#{proposer} council=#{size}"
|
|
18
|
+
{ proposal_id: id, category: category, status: :open }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def vote_on_proposal(proposal_id:, voter:, approve:, **)
|
|
22
|
+
result = proposal_store.vote(proposal_id, voter: voter, approve: approve)
|
|
23
|
+
case result
|
|
24
|
+
when nil
|
|
25
|
+
Legion::Logging.debug "[governance] vote failed: proposal=#{proposal_id[0..7]} not found or closed"
|
|
26
|
+
{ error: :not_found_or_closed }
|
|
27
|
+
when :already_voted
|
|
28
|
+
Legion::Logging.debug "[governance] vote failed: proposal=#{proposal_id[0..7]} voter=#{voter} already voted"
|
|
29
|
+
{ error: :already_voted }
|
|
30
|
+
else
|
|
31
|
+
Legion::Logging.info "[governance] vote: proposal=#{proposal_id[0..7]} voter=#{voter} approve=#{approve} resolution=#{result}"
|
|
32
|
+
{ voted: true, resolution: result }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def get_proposal(proposal_id:, **)
|
|
37
|
+
prop = proposal_store.get(proposal_id)
|
|
38
|
+
Legion::Logging.debug "[governance] get: proposal=#{proposal_id[0..7]} found=#{!prop.nil?}"
|
|
39
|
+
prop ? { found: true, proposal: prop } : { found: false }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def open_proposals(**)
|
|
43
|
+
props = proposal_store.open_proposals
|
|
44
|
+
Legion::Logging.debug "[governance] open proposals: count=#{props.size}"
|
|
45
|
+
{ proposals: props, count: props.size }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def timeout_proposals(**)
|
|
49
|
+
open = proposal_store.open_proposals
|
|
50
|
+
timed = open.select { |p| Time.now.utc - p[:created_at] > Helpers::Layers::VOTE_TIMEOUT }
|
|
51
|
+
timed.each { |p| proposal_store.resolve_timed_out(p[:proposal_id]) }
|
|
52
|
+
timed_ids = timed.map { |p| p[:proposal_id] }
|
|
53
|
+
Legion::Logging.debug "[governance] vote timeout sweep: open=#{open.size} timed_out=#{timed.size}"
|
|
54
|
+
{ checked: open.size, timed_out: timed.size, timed_out_ids: timed_ids }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_action(layer:, action: nil, _context: {}, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
58
|
+
return { error: :invalid_layer } unless Helpers::Layers.valid_layer?(layer)
|
|
59
|
+
|
|
60
|
+
result = case layer
|
|
61
|
+
when :agent_validation
|
|
62
|
+
{ allowed: true, layer: layer, reason: :self_validated }
|
|
63
|
+
when :anomaly_detection
|
|
64
|
+
{ allowed: true, layer: layer, reason: :no_anomaly }
|
|
65
|
+
when :human_deliberation
|
|
66
|
+
{ allowed: false, layer: layer, reason: :requires_human_approval }
|
|
67
|
+
when :transparency
|
|
68
|
+
{ allowed: true, layer: layer, reason: :logged, audit_required: true }
|
|
69
|
+
end
|
|
70
|
+
Legion::Logging.debug "[governance] validate: layer=#{layer} allowed=#{result[:allowed]} reason=#{result[:reason]}"
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def proposal_store
|
|
77
|
+
@proposal_store ||= Helpers::Proposal.new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/version'
|
|
4
|
+
require 'legion/extensions/governance/helpers/layers'
|
|
5
|
+
require 'legion/extensions/governance/helpers/proposal'
|
|
6
|
+
require 'legion/extensions/governance/runners/governance'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Governance
|
|
11
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every; end # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require_relative '../../../../../lib/legion/extensions/governance/actors/vote_timeout'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Governance::Actor::VoteTimeout do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
describe '#runner_class' do
|
|
19
|
+
it { expect(actor.runner_class).to eq Legion::Extensions::Governance::Runners::Governance }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#runner_function' do
|
|
23
|
+
it { expect(actor.runner_function).to eq 'timeout_proposals' }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#time' do
|
|
27
|
+
it { expect(actor.time).to eq 300 }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#run_now?' do
|
|
31
|
+
it { expect(actor.run_now?).to be false }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#use_runner?' do
|
|
35
|
+
it { expect(actor.use_runner?).to be false }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#check_subtask?' do
|
|
39
|
+
it { expect(actor.check_subtask?).to be false }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#generate_task?' do
|
|
43
|
+
it { expect(actor.generate_task?).to be false }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Governance::Client do
|
|
6
|
+
it 'responds to governance runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:create_proposal)
|
|
9
|
+
expect(client).to respond_to(:vote_on_proposal)
|
|
10
|
+
expect(client).to respond_to(:get_proposal)
|
|
11
|
+
expect(client).to respond_to(:open_proposals)
|
|
12
|
+
expect(client).to respond_to(:validate_action)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/helpers/layers'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Governance::Helpers::Layers do
|
|
6
|
+
describe 'GOVERNANCE_LAYERS' do
|
|
7
|
+
it 'contains four layers as symbols' do
|
|
8
|
+
expect(described_class::GOVERNANCE_LAYERS.size).to eq(4)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'includes agent_validation' do
|
|
12
|
+
expect(described_class::GOVERNANCE_LAYERS).to include(:agent_validation)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'includes anomaly_detection' do
|
|
16
|
+
expect(described_class::GOVERNANCE_LAYERS).to include(:anomaly_detection)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'includes human_deliberation' do
|
|
20
|
+
expect(described_class::GOVERNANCE_LAYERS).to include(:human_deliberation)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'includes transparency' do
|
|
24
|
+
expect(described_class::GOVERNANCE_LAYERS).to include(:transparency)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'is frozen' do
|
|
28
|
+
expect(described_class::GOVERNANCE_LAYERS).to be_frozen
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'PROPOSAL_CATEGORIES' do
|
|
33
|
+
it 'contains five categories' do
|
|
34
|
+
expect(described_class::PROPOSAL_CATEGORIES.size).to eq(5)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'includes policy_change' do
|
|
38
|
+
expect(described_class::PROPOSAL_CATEGORIES).to include(:policy_change)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'includes resource_allocation' do
|
|
42
|
+
expect(described_class::PROPOSAL_CATEGORIES).to include(:resource_allocation)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'includes access_control' do
|
|
46
|
+
expect(described_class::PROPOSAL_CATEGORIES).to include(:access_control)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'includes emergency' do
|
|
50
|
+
expect(described_class::PROPOSAL_CATEGORIES).to include(:emergency)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'includes protocol_update' do
|
|
54
|
+
expect(described_class::PROPOSAL_CATEGORIES).to include(:protocol_update)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'is frozen' do
|
|
58
|
+
expect(described_class::PROPOSAL_CATEGORIES).to be_frozen
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'numeric constants' do
|
|
63
|
+
it 'sets MIN_COUNCIL_SIZE to 3' do
|
|
64
|
+
expect(described_class::MIN_COUNCIL_SIZE).to eq(3)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'sets QUORUM_FRACTION to 0.66' do
|
|
68
|
+
expect(described_class::QUORUM_FRACTION).to eq(0.66)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'sets VOTE_TIMEOUT to 86400' do
|
|
72
|
+
expect(described_class::VOTE_TIMEOUT).to eq(86_400)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '.valid_layer?' do
|
|
77
|
+
it 'returns true for agent_validation' do
|
|
78
|
+
expect(described_class.valid_layer?(:agent_validation)).to be true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns true for anomaly_detection' do
|
|
82
|
+
expect(described_class.valid_layer?(:anomaly_detection)).to be true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns true for human_deliberation' do
|
|
86
|
+
expect(described_class.valid_layer?(:human_deliberation)).to be true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns true for transparency' do
|
|
90
|
+
expect(described_class.valid_layer?(:transparency)).to be true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns false for unknown layer symbol' do
|
|
94
|
+
expect(described_class.valid_layer?(:unknown)).to be false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns false for nil' do
|
|
98
|
+
expect(described_class.valid_layer?(nil)).to be false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns false for string version of valid layer' do
|
|
102
|
+
expect(described_class.valid_layer?('agent_validation')).to be false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '.valid_category?' do
|
|
107
|
+
it 'returns true for policy_change' do
|
|
108
|
+
expect(described_class.valid_category?(:policy_change)).to be true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'returns true for resource_allocation' do
|
|
112
|
+
expect(described_class.valid_category?(:resource_allocation)).to be true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'returns true for access_control' do
|
|
116
|
+
expect(described_class.valid_category?(:access_control)).to be true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'returns true for emergency' do
|
|
120
|
+
expect(described_class.valid_category?(:emergency)).to be true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'returns true for protocol_update' do
|
|
124
|
+
expect(described_class.valid_category?(:protocol_update)).to be true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns false for unknown category' do
|
|
128
|
+
expect(described_class.valid_category?(:invalid)).to be false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns false for nil' do
|
|
132
|
+
expect(described_class.valid_category?(nil)).to be false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'returns false for string version of valid category' do
|
|
136
|
+
expect(described_class.valid_category?('emergency')).to be false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe '.quorum_met?' do
|
|
141
|
+
context 'when council_size is below minimum' do
|
|
142
|
+
it 'returns false for council_size of 2' do
|
|
143
|
+
expect(described_class.quorum_met?(2, 2)).to be false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'returns false for council_size of 1' do
|
|
147
|
+
expect(described_class.quorum_met?(1, 1)).to be false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'returns false for council_size of 0' do
|
|
151
|
+
expect(described_class.quorum_met?(0, 0)).to be false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context 'with minimum council size of 3' do
|
|
156
|
+
it 'returns true when 2 of 3 vote (meets ceil(3 * 0.66) = 2)' do
|
|
157
|
+
expect(described_class.quorum_met?(2, 3)).to be true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'returns true when all 3 vote' do
|
|
161
|
+
expect(described_class.quorum_met?(3, 3)).to be true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'returns false when only 1 of 3 vote' do
|
|
165
|
+
expect(described_class.quorum_met?(1, 3)).to be false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context 'with larger council sizes' do
|
|
170
|
+
it 'requires ceil(4 * 0.66) = 3 votes for council of 4' do
|
|
171
|
+
expect(described_class.quorum_met?(3, 4)).to be true
|
|
172
|
+
expect(described_class.quorum_met?(2, 4)).to be false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'requires ceil(5 * 0.66) = 4 votes for council of 5' do
|
|
176
|
+
expect(described_class.quorum_met?(4, 5)).to be true
|
|
177
|
+
expect(described_class.quorum_met?(3, 5)).to be false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'requires ceil(9 * 0.66) = 6 votes for council of 9' do
|
|
181
|
+
expect(described_class.quorum_met?(6, 9)).to be true
|
|
182
|
+
expect(described_class.quorum_met?(5, 9)).to be false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'returns false for zero votes regardless of council size' do
|
|
187
|
+
expect(described_class.quorum_met?(0, 3)).to be false
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/helpers/layers'
|
|
4
|
+
require 'legion/extensions/governance/helpers/proposal'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Governance::Helpers::Proposal do
|
|
7
|
+
subject(:proposal_store) { described_class.new }
|
|
8
|
+
|
|
9
|
+
describe '#initialize' do
|
|
10
|
+
it 'starts with an empty proposals hash' do
|
|
11
|
+
expect(proposal_store.proposals).to eq({})
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#create' do
|
|
16
|
+
let(:created_id) do
|
|
17
|
+
proposal_store.create(category: :policy_change, description: 'Enable new policy', proposer: 'agent-1')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns a UUID string' do
|
|
21
|
+
expect(created_id).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'stores the proposal under the returned ID' do
|
|
25
|
+
expect(proposal_store.proposals[created_id]).not_to be_nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'stores the correct category' do
|
|
29
|
+
expect(proposal_store.proposals[created_id][:category]).to eq(:policy_change)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'stores the correct description' do
|
|
33
|
+
expect(proposal_store.proposals[created_id][:description]).to eq('Enable new policy')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'stores the correct proposer' do
|
|
37
|
+
expect(proposal_store.proposals[created_id][:proposer]).to eq('agent-1')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'defaults council_size to MIN_COUNCIL_SIZE' do
|
|
41
|
+
expect(proposal_store.proposals[created_id][:council_size])
|
|
42
|
+
.to eq(Legion::Extensions::Governance::Helpers::Layers::MIN_COUNCIL_SIZE)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'uses the provided council_size when specified' do
|
|
46
|
+
id = proposal_store.create(category: :emergency, description: 'urgent', proposer: 'agent-x', council_size: 7)
|
|
47
|
+
expect(proposal_store.proposals[id][:council_size]).to eq(7)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'initializes with status :open' do
|
|
51
|
+
expect(proposal_store.proposals[created_id][:status]).to eq(:open)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'initializes votes_for as empty array' do
|
|
55
|
+
expect(proposal_store.proposals[created_id][:votes_for]).to eq([])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'initializes votes_against as empty array' do
|
|
59
|
+
expect(proposal_store.proposals[created_id][:votes_against]).to eq([])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'sets created_at to a Time object' do
|
|
63
|
+
expect(proposal_store.proposals[created_id][:created_at]).to be_a(Time)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'sets resolved_at to nil initially' do
|
|
67
|
+
expect(proposal_store.proposals[created_id][:resolved_at]).to be_nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'generates unique IDs for separate calls' do
|
|
71
|
+
id1 = proposal_store.create(category: :policy_change, description: 'a', proposer: 'x')
|
|
72
|
+
id2 = proposal_store.create(category: :policy_change, description: 'b', proposer: 'y')
|
|
73
|
+
expect(id1).not_to eq(id2)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#get' do
|
|
78
|
+
it 'returns the proposal hash for a valid ID' do
|
|
79
|
+
id = proposal_store.create(category: :policy_change, description: 'test', proposer: 'agent-1')
|
|
80
|
+
result = proposal_store.get(id)
|
|
81
|
+
expect(result[:proposal_id]).to eq(id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns nil for an unknown ID' do
|
|
85
|
+
expect(proposal_store.get('nonexistent-uuid')).to be_nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#open_proposals' do
|
|
90
|
+
it 'returns empty array when no proposals exist' do
|
|
91
|
+
expect(proposal_store.open_proposals).to eq([])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns all open proposals' do
|
|
95
|
+
proposal_store.create(category: :policy_change, description: 'first', proposer: 'a1')
|
|
96
|
+
proposal_store.create(category: :emergency, description: 'second', proposer: 'a2')
|
|
97
|
+
expect(proposal_store.open_proposals.size).to eq(2)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'excludes resolved proposals' do
|
|
101
|
+
id = proposal_store.create(category: :policy_change, description: 'vote me', proposer: 'a1', council_size: 3)
|
|
102
|
+
proposal_store.vote(id, voter: 'v1', approve: true)
|
|
103
|
+
proposal_store.vote(id, voter: 'v2', approve: true)
|
|
104
|
+
expect(proposal_store.open_proposals).to be_empty
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#vote' do
|
|
109
|
+
let(:proposal_id) do
|
|
110
|
+
proposal_store.create(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'returns nil for an unknown proposal ID' do
|
|
114
|
+
expect(proposal_store.vote('no-such-id', voter: 'v1', approve: true)).to be_nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns :already_voted when the same voter votes twice' do
|
|
118
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
119
|
+
result = proposal_store.vote(proposal_id, voter: 'v1', approve: false)
|
|
120
|
+
expect(result).to eq(:already_voted)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'prevents double voting even when switching approve/reject' do
|
|
124
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: false)
|
|
125
|
+
result = proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
126
|
+
expect(result).to eq(:already_voted)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'adds approve vote to votes_for' do
|
|
130
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
131
|
+
expect(proposal_store.proposals[proposal_id][:votes_for]).to include('v1')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'adds reject vote to votes_against' do
|
|
135
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: false)
|
|
136
|
+
expect(proposal_store.proposals[proposal_id][:votes_against]).to include('v1')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns :pending when quorum is not yet met' do
|
|
140
|
+
result = proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
141
|
+
expect(result).to eq(:pending)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context 'when quorum for approval is met' do
|
|
145
|
+
it 'returns :approved and sets status' do
|
|
146
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
147
|
+
result = proposal_store.vote(proposal_id, voter: 'v2', approve: true)
|
|
148
|
+
expect(result).to eq(:approved)
|
|
149
|
+
expect(proposal_store.proposals[proposal_id][:status]).to eq(:approved)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'sets resolved_at on approval' do
|
|
153
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
154
|
+
proposal_store.vote(proposal_id, voter: 'v2', approve: true)
|
|
155
|
+
expect(proposal_store.proposals[proposal_id][:resolved_at]).to be_a(Time)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when quorum for rejection is met' do
|
|
160
|
+
it 'returns :rejected when enough votes against' do
|
|
161
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: false)
|
|
162
|
+
result = proposal_store.vote(proposal_id, voter: 'v2', approve: false)
|
|
163
|
+
expect(result).to eq(:rejected)
|
|
164
|
+
expect(proposal_store.proposals[proposal_id][:status]).to eq(:rejected)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'sets resolved_at on rejection' do
|
|
168
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: false)
|
|
169
|
+
proposal_store.vote(proposal_id, voter: 'v2', approve: false)
|
|
170
|
+
expect(proposal_store.proposals[proposal_id][:resolved_at]).to be_a(Time)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'returns :rejected when all council members voted and no quorum for approval' do
|
|
174
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
175
|
+
proposal_store.vote(proposal_id, voter: 'v2', approve: false)
|
|
176
|
+
result = proposal_store.vote(proposal_id, voter: 'v3', approve: false)
|
|
177
|
+
expect(result).to eq(:rejected)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'returns nil when voting on a resolved (approved) proposal' do
|
|
182
|
+
proposal_store.vote(proposal_id, voter: 'v1', approve: true)
|
|
183
|
+
proposal_store.vote(proposal_id, voter: 'v2', approve: true)
|
|
184
|
+
result = proposal_store.vote(proposal_id, voter: 'v3', approve: true)
|
|
185
|
+
expect(result).to be_nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/governance/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Governance::Runners::Governance do
|
|
6
|
+
let(:client) { Legion::Extensions::Governance::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#create_proposal' do
|
|
9
|
+
it 'creates a proposal' do
|
|
10
|
+
result = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1')
|
|
11
|
+
expect(result[:proposal_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
12
|
+
expect(result[:status]).to eq(:open)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'rejects invalid category' do
|
|
16
|
+
result = client.create_proposal(category: :invalid, description: 'test', proposer: 'agent-1')
|
|
17
|
+
expect(result[:error]).to eq(:invalid_category)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#vote_on_proposal' do
|
|
22
|
+
it 'records a vote' do
|
|
23
|
+
prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
|
|
24
|
+
result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
|
|
25
|
+
expect(result[:voted]).to be true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'prevents double voting' do
|
|
29
|
+
prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
|
|
30
|
+
client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
|
|
31
|
+
result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
|
|
32
|
+
expect(result[:error]).to eq(:already_voted)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'approves with quorum' do
|
|
36
|
+
prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
|
|
37
|
+
client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v1', approve: true)
|
|
38
|
+
result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v2', approve: true)
|
|
39
|
+
expect(result[:resolution]).to eq(:approved)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#validate_action' do
|
|
44
|
+
it 'allows agent_validation' do
|
|
45
|
+
result = client.validate_action(layer: :agent_validation, action: 'test')
|
|
46
|
+
expect(result[:allowed]).to be true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'blocks human_deliberation' do
|
|
50
|
+
result = client.validate_action(layer: :human_deliberation, action: 'test')
|
|
51
|
+
expect(result[:allowed]).to be false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'rejects invalid layer' do
|
|
55
|
+
result = client.validate_action(layer: :invalid, action: 'test')
|
|
56
|
+
expect(result[:error]).to eq(:invalid_layer)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#open_proposals' do
|
|
61
|
+
it 'lists open proposals' do
|
|
62
|
+
client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1')
|
|
63
|
+
result = client.open_proposals
|
|
64
|
+
expect(result[:count]).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#timeout_proposals' do
|
|
69
|
+
it 'returns zero timed_out when no proposals exist' do
|
|
70
|
+
result = client.timeout_proposals
|
|
71
|
+
expect(result[:checked]).to eq(0)
|
|
72
|
+
expect(result[:timed_out]).to eq(0)
|
|
73
|
+
expect(result[:timed_out_ids]).to eq([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns zero timed_out for recently created proposals' do
|
|
77
|
+
client.create_proposal(category: :policy_change, description: 'fresh', proposer: 'agent-1')
|
|
78
|
+
result = client.timeout_proposals
|
|
79
|
+
expect(result[:timed_out]).to eq(0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'times out proposals older than VOTE_TIMEOUT' do
|
|
83
|
+
prop = client.create_proposal(category: :policy_change, description: 'old', proposer: 'agent-1')
|
|
84
|
+
store = client.instance_variable_get(:@proposal_store)
|
|
85
|
+
store.proposals[prop[:proposal_id]][:created_at] = Time.now.utc - (Legion::Extensions::Governance::Helpers::Layers::VOTE_TIMEOUT + 1)
|
|
86
|
+
result = client.timeout_proposals
|
|
87
|
+
expect(result[:timed_out]).to eq(1)
|
|
88
|
+
expect(result[:timed_out_ids]).to include(prop[:proposal_id])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'does not time out already-resolved proposals' do
|
|
92
|
+
prop = client.create_proposal(category: :policy_change, description: 'resolved', proposer: 'agent-1', council_size: 3)
|
|
93
|
+
store = client.instance_variable_get(:@proposal_store)
|
|
94
|
+
store.proposals[prop[:proposal_id]][:created_at] = Time.now.utc - (Legion::Extensions::Governance::Helpers::Layers::VOTE_TIMEOUT + 1)
|
|
95
|
+
client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v1', approve: true)
|
|
96
|
+
client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v2', approve: true)
|
|
97
|
+
result = client.timeout_proposals
|
|
98
|
+
expect(result[:timed_out]).to eq(0)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/governance'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-governance
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Four-layer distributed governance protocol for brain-modeled agentic
|
|
27
|
+
AI
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- lex-governance.gemspec
|
|
36
|
+
- lib/legion/extensions/governance.rb
|
|
37
|
+
- lib/legion/extensions/governance/actors/vote_timeout.rb
|
|
38
|
+
- lib/legion/extensions/governance/client.rb
|
|
39
|
+
- lib/legion/extensions/governance/helpers/layers.rb
|
|
40
|
+
- lib/legion/extensions/governance/helpers/proposal.rb
|
|
41
|
+
- lib/legion/extensions/governance/runners/governance.rb
|
|
42
|
+
- lib/legion/extensions/governance/version.rb
|
|
43
|
+
- spec/legion/extensions/governance/actors/vote_timeout_spec.rb
|
|
44
|
+
- spec/legion/extensions/governance/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/governance/helpers/layers_spec.rb
|
|
46
|
+
- spec/legion/extensions/governance/helpers/proposal_spec.rb
|
|
47
|
+
- spec/legion/extensions/governance/runners/governance_spec.rb
|
|
48
|
+
- spec/spec_helper.rb
|
|
49
|
+
homepage: https://github.com/LegionIO/lex-governance
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://github.com/LegionIO/lex-governance
|
|
54
|
+
source_code_uri: https://github.com/LegionIO/lex-governance
|
|
55
|
+
documentation_uri: https://github.com/LegionIO/lex-governance
|
|
56
|
+
changelog_uri: https://github.com/LegionIO/lex-governance
|
|
57
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-governance/issues
|
|
58
|
+
rubygems_mfa_required: 'true'
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.4'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.9
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: LEX Governance
|
|
76
|
+
test_files: []
|