lex-trust 0.1.0

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.
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Trust::Helpers::TrustMap do
6
+ subject(:map) { described_class.new }
7
+
8
+ let(:agent_id) { 'agent-001' }
9
+
10
+ describe '#initialize' do
11
+ it 'starts with an empty entries hash' do
12
+ expect(map.entries).to eq({})
13
+ end
14
+ end
15
+
16
+ describe '#get' do
17
+ it 'returns nil for an unknown agent' do
18
+ expect(map.get(agent_id)).to be_nil
19
+ end
20
+
21
+ it 'returns nil for a known agent in a different domain' do
22
+ map.get_or_create(agent_id, domain: :code)
23
+ expect(map.get(agent_id, domain: :ops)).to be_nil
24
+ end
25
+
26
+ it 'returns the entry after it has been created' do
27
+ map.get_or_create(agent_id)
28
+ expect(map.get(agent_id)).not_to be_nil
29
+ end
30
+
31
+ it 'retrieves an entry for the correct domain' do
32
+ map.get_or_create(agent_id, domain: :code)
33
+ entry = map.get(agent_id, domain: :code)
34
+ expect(entry[:domain]).to eq(:code)
35
+ end
36
+
37
+ it 'defaults domain to :general' do
38
+ map.get_or_create(agent_id)
39
+ entry = map.get(agent_id)
40
+ expect(entry[:domain]).to eq(:general)
41
+ end
42
+ end
43
+
44
+ describe '#get_or_create' do
45
+ it 'creates a new entry for a new agent' do
46
+ entry = map.get_or_create(agent_id)
47
+ expect(entry).not_to be_nil
48
+ end
49
+
50
+ it 'returns the same entry on subsequent calls' do
51
+ first = map.get_or_create(agent_id)
52
+ second = map.get_or_create(agent_id)
53
+ expect(first).to equal(second)
54
+ end
55
+
56
+ it 'initializes composite to NEUTRAL_TRUST' do
57
+ entry = map.get_or_create(agent_id)
58
+ expect(entry[:composite]).to eq(Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST)
59
+ end
60
+
61
+ it 'creates independent entries per domain' do
62
+ general = map.get_or_create(agent_id, domain: :general)
63
+ code = map.get_or_create(agent_id, domain: :code)
64
+ expect(general).not_to equal(code)
65
+ end
66
+
67
+ it 'creates independent entries per agent' do
68
+ a = map.get_or_create('agent-a')
69
+ b = map.get_or_create('agent-b')
70
+ expect(a).not_to equal(b)
71
+ end
72
+ end
73
+
74
+ describe '#record_interaction' do
75
+ context 'with a positive interaction' do
76
+ it 'increments interaction_count by 1' do
77
+ map.record_interaction(agent_id, positive: true)
78
+ expect(map.get(agent_id)[:interaction_count]).to eq(1)
79
+ end
80
+
81
+ it 'increments positive_count by 1' do
82
+ map.record_interaction(agent_id, positive: true)
83
+ expect(map.get(agent_id)[:positive_count]).to eq(1)
84
+ end
85
+
86
+ it 'does not increment negative_count' do
87
+ map.record_interaction(agent_id, positive: true)
88
+ expect(map.get(agent_id)[:negative_count]).to eq(0)
89
+ end
90
+
91
+ it 'increases all dimension values above NEUTRAL_TRUST' do
92
+ map.record_interaction(agent_id, positive: true)
93
+ entry = map.get(agent_id)
94
+ Legion::Extensions::Trust::Helpers::TrustModel::TRUST_DIMENSIONS.each do |dim|
95
+ expect(entry[:dimensions][dim]).to be > Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST
96
+ end
97
+ end
98
+
99
+ it 'increases composite above NEUTRAL_TRUST' do
100
+ map.record_interaction(agent_id, positive: true)
101
+ expect(map.get(agent_id)[:composite]).to be > Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST
102
+ end
103
+
104
+ it 'sets last_interaction to a recent time' do
105
+ before = Time.now.utc
106
+ map.record_interaction(agent_id, positive: true)
107
+ expect(map.get(agent_id)[:last_interaction]).to be >= before
108
+ end
109
+ end
110
+
111
+ context 'with a negative interaction' do
112
+ it 'increments interaction_count by 1' do
113
+ map.record_interaction(agent_id, positive: false)
114
+ expect(map.get(agent_id)[:interaction_count]).to eq(1)
115
+ end
116
+
117
+ it 'increments negative_count by 1' do
118
+ map.record_interaction(agent_id, positive: false)
119
+ expect(map.get(agent_id)[:negative_count]).to eq(1)
120
+ end
121
+
122
+ it 'does not increment positive_count' do
123
+ map.record_interaction(agent_id, positive: false)
124
+ expect(map.get(agent_id)[:positive_count]).to eq(0)
125
+ end
126
+
127
+ it 'decreases composite below NEUTRAL_TRUST' do
128
+ map.record_interaction(agent_id, positive: false)
129
+ expect(map.get(agent_id)[:composite]).to be < Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST
130
+ end
131
+
132
+ it 'applies the asymmetric penalty (penalty > reinforcement)' do
133
+ map.record_interaction(agent_id, positive: true)
134
+ positive_composite = map.get(agent_id)[:composite]
135
+ map2 = described_class.new
136
+ map2.record_interaction(agent_id, positive: false)
137
+ negative_composite = map2.get(agent_id)[:composite]
138
+ delta_positive = positive_composite - Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST
139
+ delta_negative = Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST - negative_composite
140
+ expect(delta_negative).to be > delta_positive
141
+ end
142
+ end
143
+
144
+ it 'creates the entry if it does not exist' do
145
+ map.record_interaction('new-agent', positive: true)
146
+ expect(map.get('new-agent')).not_to be_nil
147
+ end
148
+
149
+ it 'tracks trust separately per domain' do
150
+ map.record_interaction(agent_id, positive: true, domain: :code)
151
+ map.record_interaction(agent_id, positive: false, domain: :ops)
152
+ expect(map.get(agent_id, domain: :code)[:composite]).to be > map.get(agent_id, domain: :ops)[:composite]
153
+ end
154
+ end
155
+
156
+ describe '#reinforce_dimension' do
157
+ it 'increases the targeted dimension' do
158
+ map.get_or_create(agent_id)
159
+ map.reinforce_dimension(agent_id, dimension: :competence, amount: 0.2)
160
+ entry = map.get(agent_id)
161
+ expect(entry[:dimensions][:competence]).to be > Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST
162
+ end
163
+
164
+ it 'does not change non-targeted dimensions' do
165
+ map.get_or_create(agent_id)
166
+ map.reinforce_dimension(agent_id, dimension: :competence, amount: 0.1)
167
+ entry = map.get(agent_id)
168
+ expect(entry[:dimensions][:reliability]).to eq(Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST)
169
+ end
170
+
171
+ it 'updates composite after reinforcement' do
172
+ map.get_or_create(agent_id)
173
+ original_composite = map.get(agent_id)[:composite]
174
+ map.reinforce_dimension(agent_id, dimension: :competence, amount: 0.2)
175
+ expect(map.get(agent_id)[:composite]).to be > original_composite
176
+ end
177
+
178
+ it 'ignores an invalid dimension' do
179
+ map.get_or_create(agent_id)
180
+ original = map.get(agent_id)[:composite]
181
+ map.reinforce_dimension(agent_id, dimension: :invalid_dim)
182
+ expect(map.get(agent_id)[:composite]).to eq(original)
183
+ end
184
+
185
+ it 'clamps dimension to 1.0 maximum' do
186
+ map.get_or_create(agent_id)
187
+ map.reinforce_dimension(agent_id, dimension: :reliability, amount: 5.0)
188
+ expect(map.get(agent_id)[:dimensions][:reliability]).to eq(1.0)
189
+ end
190
+ end
191
+
192
+ describe '#decay_all' do
193
+ it 'returns 0 when no entries exist' do
194
+ expect(map.decay_all).to eq(0)
195
+ end
196
+
197
+ it 'returns the number of entries decayed' do
198
+ map.get_or_create('agent-a')
199
+ map.get_or_create('agent-b')
200
+ expect(map.decay_all).to eq(2)
201
+ end
202
+
203
+ it 'reduces each dimension by TRUST_DECAY_RATE' do
204
+ map.get_or_create(agent_id)
205
+ map.decay_all
206
+ entry = map.get(agent_id)
207
+ Legion::Extensions::Trust::Helpers::TrustModel::TRUST_DIMENSIONS.each do |dim|
208
+ expected = Legion::Extensions::Trust::Helpers::TrustModel::NEUTRAL_TRUST -
209
+ Legion::Extensions::Trust::Helpers::TrustModel::TRUST_DECAY_RATE
210
+ expect(entry[:dimensions][dim]).to be_within(0.0001).of(expected)
211
+ end
212
+ end
213
+
214
+ it 'floors dimensions at 0.0 when trust is already near-zero' do
215
+ map.get_or_create(agent_id)
216
+ 200.times { map.decay_all }
217
+ entry = map.get(agent_id)
218
+ Legion::Extensions::Trust::Helpers::TrustModel::TRUST_DIMENSIONS.each do |dim|
219
+ expect(entry[:dimensions][dim]).to eq(0.0)
220
+ end
221
+ end
222
+
223
+ it 'recomputes composite after decay' do
224
+ map.get_or_create(agent_id)
225
+ before = map.get(agent_id)[:composite]
226
+ map.decay_all
227
+ expect(map.get(agent_id)[:composite]).to be < before
228
+ end
229
+ end
230
+
231
+ describe '#trusted_agents' do
232
+ it 'returns empty array when no entries exist' do
233
+ expect(map.trusted_agents).to be_empty
234
+ end
235
+
236
+ it 'returns entries with composite >= min_trust' do
237
+ 5.times { map.record_interaction(agent_id, positive: true) }
238
+ result = map.trusted_agents
239
+ expect(result).not_to be_empty
240
+ result.each do |entry|
241
+ expect(entry[:composite]).to be >= Legion::Extensions::Trust::Helpers::TrustModel::TRUST_CONSIDER_THRESHOLD
242
+ end
243
+ end
244
+
245
+ it 'excludes entries below min_trust' do
246
+ map.record_interaction(agent_id, positive: false)
247
+ expect(map.trusted_agents).to be_empty
248
+ end
249
+
250
+ it 'filters by domain' do
251
+ 5.times { map.record_interaction(agent_id, positive: true, domain: :code) }
252
+ map.record_interaction('other-agent', positive: true, domain: :ops)
253
+ result = map.trusted_agents(domain: :code)
254
+ expect(result.all? { |e| e[:domain] == :code }).to be true
255
+ end
256
+
257
+ it 'sorts by composite descending' do
258
+ 5.times { map.record_interaction('agent-high', positive: true) }
259
+ 3.times { map.record_interaction('agent-low', positive: true) }
260
+ result = map.trusted_agents
261
+ composites = result.map { |e| e[:composite] }
262
+ expect(composites).to eq(composites.sort.reverse)
263
+ end
264
+
265
+ it 'accepts a custom min_trust threshold' do
266
+ 5.times { map.record_interaction(agent_id, positive: true) }
267
+ strict_result = map.trusted_agents(min_trust: 0.9)
268
+ loose_result = map.trusted_agents(min_trust: 0.3)
269
+ expect(strict_result.size).to be <= loose_result.size
270
+ end
271
+ end
272
+
273
+ describe '#delegatable_agents' do
274
+ it 'requires TRUST_DELEGATE_THRESHOLD (higher than trusted_agents default)' do
275
+ 5.times { map.record_interaction(agent_id, positive: true) }
276
+ trusted = map.trusted_agents.size
277
+ delegatable = map.delegatable_agents.size
278
+ expect(delegatable).to be <= trusted
279
+ end
280
+
281
+ it 'returns empty array when no entries meet the delegation threshold' do
282
+ map.record_interaction(agent_id, positive: true)
283
+ expect(map.delegatable_agents).to be_empty
284
+ end
285
+ end
286
+
287
+ describe '#count' do
288
+ it 'returns 0 for an empty map' do
289
+ expect(map.count).to eq(0)
290
+ end
291
+
292
+ it 'counts total entries across all agents and domains' do
293
+ map.get_or_create('a', domain: :general)
294
+ map.get_or_create('a', domain: :code)
295
+ map.get_or_create('b', domain: :general)
296
+ expect(map.count).to eq(3)
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Trust::Helpers::TrustModel do
6
+ describe 'TRUST_DIMENSIONS' do
7
+ it 'is a frozen array of symbols' do
8
+ expect(described_class::TRUST_DIMENSIONS).to be_a(Array)
9
+ expect(described_class::TRUST_DIMENSIONS).to be_frozen
10
+ end
11
+
12
+ it 'contains exactly four dimensions' do
13
+ expect(described_class::TRUST_DIMENSIONS.size).to eq(4)
14
+ end
15
+
16
+ it 'includes reliability' do
17
+ expect(described_class::TRUST_DIMENSIONS).to include(:reliability)
18
+ end
19
+
20
+ it 'includes competence' do
21
+ expect(described_class::TRUST_DIMENSIONS).to include(:competence)
22
+ end
23
+
24
+ it 'includes integrity' do
25
+ expect(described_class::TRUST_DIMENSIONS).to include(:integrity)
26
+ end
27
+
28
+ it 'includes benevolence' do
29
+ expect(described_class::TRUST_DIMENSIONS).to include(:benevolence)
30
+ end
31
+ end
32
+
33
+ describe 'threshold constants' do
34
+ it 'TRUST_CONSIDER_THRESHOLD is 0.3' do
35
+ expect(described_class::TRUST_CONSIDER_THRESHOLD).to eq(0.3)
36
+ end
37
+
38
+ it 'TRUST_DELEGATE_THRESHOLD is 0.7' do
39
+ expect(described_class::TRUST_DELEGATE_THRESHOLD).to eq(0.7)
40
+ end
41
+
42
+ it 'delegate threshold is higher than consider threshold' do
43
+ expect(described_class::TRUST_DELEGATE_THRESHOLD).to be > described_class::TRUST_CONSIDER_THRESHOLD
44
+ end
45
+
46
+ it 'TRUST_DECAY_RATE is 0.005' do
47
+ expect(described_class::TRUST_DECAY_RATE).to eq(0.005)
48
+ end
49
+
50
+ it 'TRUST_REINFORCEMENT is 0.05' do
51
+ expect(described_class::TRUST_REINFORCEMENT).to eq(0.05)
52
+ end
53
+
54
+ it 'TRUST_PENALTY is 0.15' do
55
+ expect(described_class::TRUST_PENALTY).to eq(0.15)
56
+ end
57
+
58
+ it 'penalty is asymmetrically larger than reinforcement' do
59
+ expect(described_class::TRUST_PENALTY).to be > described_class::TRUST_REINFORCEMENT
60
+ end
61
+
62
+ it 'NEUTRAL_TRUST is 0.3' do
63
+ expect(described_class::NEUTRAL_TRUST).to eq(0.3)
64
+ end
65
+
66
+ it 'NEUTRAL_TRUST equals TRUST_CONSIDER_THRESHOLD (new agents are borderline trustworthy)' do
67
+ expect(described_class::NEUTRAL_TRUST).to eq(described_class::TRUST_CONSIDER_THRESHOLD)
68
+ end
69
+ end
70
+
71
+ describe '.new_trust_entry' do
72
+ let(:entry) { described_class.new_trust_entry(agent_id: 'agent-42') }
73
+
74
+ it 'sets agent_id' do
75
+ expect(entry[:agent_id]).to eq('agent-42')
76
+ end
77
+
78
+ it 'defaults domain to :general' do
79
+ expect(entry[:domain]).to eq(:general)
80
+ end
81
+
82
+ it 'respects a custom domain' do
83
+ custom = described_class.new_trust_entry(agent_id: 'agent-1', domain: :code)
84
+ expect(custom[:domain]).to eq(:code)
85
+ end
86
+
87
+ it 'initializes all four dimensions to NEUTRAL_TRUST' do
88
+ described_class::TRUST_DIMENSIONS.each do |dim|
89
+ expect(entry[:dimensions][dim]).to eq(described_class::NEUTRAL_TRUST)
90
+ end
91
+ end
92
+
93
+ it 'sets composite to NEUTRAL_TRUST' do
94
+ expect(entry[:composite]).to eq(described_class::NEUTRAL_TRUST)
95
+ end
96
+
97
+ it 'starts with zero interaction_count' do
98
+ expect(entry[:interaction_count]).to eq(0)
99
+ end
100
+
101
+ it 'starts with zero positive_count' do
102
+ expect(entry[:positive_count]).to eq(0)
103
+ end
104
+
105
+ it 'starts with zero negative_count' do
106
+ expect(entry[:negative_count]).to eq(0)
107
+ end
108
+
109
+ it 'sets last_interaction to nil' do
110
+ expect(entry[:last_interaction]).to be_nil
111
+ end
112
+
113
+ it 'sets created_at to a recent UTC time' do
114
+ before = Time.now.utc
115
+ e = described_class.new_trust_entry(agent_id: 'x')
116
+ expect(e[:created_at]).to be >= before
117
+ end
118
+
119
+ it 'returns a different hash for each call' do
120
+ a = described_class.new_trust_entry(agent_id: 'a')
121
+ b = described_class.new_trust_entry(agent_id: 'b')
122
+ expect(a).not_to equal(b)
123
+ end
124
+ end
125
+
126
+ describe '.composite_score' do
127
+ it 'returns the arithmetic mean of dimension values' do
128
+ dims = { reliability: 0.8, competence: 0.6, integrity: 0.4, benevolence: 0.2 }
129
+ expect(described_class.composite_score(dims)).to be_within(0.0001).of(0.5)
130
+ end
131
+
132
+ it 'returns 1.0 when all dimensions are 1.0' do
133
+ dims = described_class::TRUST_DIMENSIONS.to_h { |d| [d, 1.0] }
134
+ expect(described_class.composite_score(dims)).to eq(1.0)
135
+ end
136
+
137
+ it 'returns 0.0 when all dimensions are 0.0' do
138
+ dims = described_class::TRUST_DIMENSIONS.to_h { |d| [d, 0.0] }
139
+ expect(described_class.composite_score(dims)).to eq(0.0)
140
+ end
141
+
142
+ it 'returns NEUTRAL_TRUST for freshly initialized dimensions' do
143
+ dims = described_class::TRUST_DIMENSIONS.to_h { |d| [d, described_class::NEUTRAL_TRUST] }
144
+ expect(described_class.composite_score(dims)).to eq(described_class::NEUTRAL_TRUST)
145
+ end
146
+
147
+ it 'returns 0.0 for an empty dimensions hash' do
148
+ expect(described_class.composite_score({})).to eq(0.0)
149
+ end
150
+ end
151
+
152
+ describe '.clamp' do
153
+ it 'returns the value when within range' do
154
+ expect(described_class.clamp(0.5)).to eq(0.5)
155
+ end
156
+
157
+ it 'clamps to 0.0 when value is negative' do
158
+ expect(described_class.clamp(-0.1)).to eq(0.0)
159
+ end
160
+
161
+ it 'clamps to 1.0 when value exceeds 1.0' do
162
+ expect(described_class.clamp(1.5)).to eq(1.0)
163
+ end
164
+
165
+ it 'returns exactly 0.0 at the lower boundary' do
166
+ expect(described_class.clamp(0.0)).to eq(0.0)
167
+ end
168
+
169
+ it 'returns exactly 1.0 at the upper boundary' do
170
+ expect(described_class.clamp(1.0)).to eq(1.0)
171
+ end
172
+
173
+ it 'accepts custom min/max bounds' do
174
+ expect(described_class.clamp(5.0, 0.0, 10.0)).to eq(5.0)
175
+ expect(described_class.clamp(15.0, 0.0, 10.0)).to eq(10.0)
176
+ expect(described_class.clamp(-1.0, 0.0, 10.0)).to eq(0.0)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/trust/client'
4
+
5
+ RSpec.describe Legion::Extensions::Trust::Runners::Trust do
6
+ let(:client) { Legion::Extensions::Trust::Client.new }
7
+
8
+ describe '#get_trust' do
9
+ it 'returns found: false for unknown agent' do
10
+ result = client.get_trust(agent_id: 'unknown')
11
+ expect(result[:found]).to be false
12
+ end
13
+ end
14
+
15
+ describe '#record_trust_interaction' do
16
+ it 'creates entry and records positive interaction' do
17
+ result = client.record_trust_interaction(agent_id: 'agent-1', positive: true)
18
+ expect(result[:positive]).to be true
19
+ expect(result[:composite]).to be > 0.3 # above neutral
20
+ end
21
+
22
+ it 'creates entry and records negative interaction' do
23
+ result = client.record_trust_interaction(agent_id: 'agent-1', positive: false)
24
+ expect(result[:composite]).to be < 0.3 # below neutral
25
+ end
26
+
27
+ it 'asymmetric: penalty > reinforcement' do
28
+ client.record_trust_interaction(agent_id: 'agent-1', positive: true)
29
+ client.get_trust(agent_id: 'agent-1')[:trust][:composite]
30
+
31
+ client.record_trust_interaction(agent_id: 'agent-1', positive: false)
32
+ after_negative = client.get_trust(agent_id: 'agent-1')[:trust][:composite]
33
+
34
+ # Net of one positive + one negative should be below starting point
35
+ expect(after_negative).to be < 0.3
36
+ end
37
+
38
+ it 'tracks per domain' do
39
+ client.record_trust_interaction(agent_id: 'agent-1', domain: :code, positive: true)
40
+ client.record_trust_interaction(agent_id: 'agent-1', domain: :ops, positive: false)
41
+
42
+ code_trust = client.get_trust(agent_id: 'agent-1', domain: :code)
43
+ ops_trust = client.get_trust(agent_id: 'agent-1', domain: :ops)
44
+
45
+ expect(code_trust[:trust][:composite]).to be > ops_trust[:trust][:composite]
46
+ end
47
+ end
48
+
49
+ describe '#reinforce_trust_dimension' do
50
+ it 'reinforces specific dimension' do
51
+ client.record_trust_interaction(agent_id: 'agent-1', positive: true)
52
+ client.reinforce_trust_dimension(agent_id: 'agent-1', dimension: :competence, amount: 0.2)
53
+ entry = client.get_trust(agent_id: 'agent-1')[:trust]
54
+ expect(entry[:dimensions][:competence]).to be > entry[:dimensions][:reliability]
55
+ end
56
+ end
57
+
58
+ describe '#decay_trust' do
59
+ it 'decays all entries' do
60
+ client.record_trust_interaction(agent_id: 'agent-1', positive: true)
61
+ before = client.get_trust(agent_id: 'agent-1')[:trust][:composite]
62
+ client.decay_trust
63
+ after = client.get_trust(agent_id: 'agent-1')[:trust][:composite]
64
+ expect(after).to be < before
65
+ end
66
+ end
67
+
68
+ describe '#trusted_agents' do
69
+ it 'returns agents above threshold' do
70
+ 5.times { client.record_trust_interaction(agent_id: 'agent-1', positive: true) }
71
+ result = client.trusted_agents
72
+ expect(result[:count]).to eq(1)
73
+ end
74
+ end
75
+
76
+ describe '#delegatable_agents' do
77
+ it 'requires higher trust for delegation' do
78
+ 3.times { client.record_trust_interaction(agent_id: 'agent-1', positive: true) }
79
+ trusted = client.trusted_agents
80
+ delegatable = client.delegatable_agents
81
+ expect(delegatable[:count]).to be <= trusted[:count]
82
+ end
83
+ end
84
+ end