lex-dissonance 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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Dissonance::Helpers::Constants do
4
+ it 'defines DISSONANCE_THRESHOLD' do
5
+ expect(described_class::DISSONANCE_THRESHOLD).to eq(0.4)
6
+ end
7
+
8
+ it 'defines MAX_BELIEFS' do
9
+ expect(described_class::MAX_BELIEFS).to eq(200)
10
+ end
11
+
12
+ it 'defines MAX_DISSONANCE_EVENTS' do
13
+ expect(described_class::MAX_DISSONANCE_EVENTS).to eq(100)
14
+ end
15
+
16
+ it 'defines DECAY_RATE' do
17
+ expect(described_class::DECAY_RATE).to eq(0.03)
18
+ end
19
+
20
+ it 'defines RESOLUTION_RELIEF' do
21
+ expect(described_class::RESOLUTION_RELIEF).to eq(0.3)
22
+ end
23
+
24
+ it 'defines RATIONALIZATION_FACTOR' do
25
+ expect(described_class::RATIONALIZATION_FACTOR).to eq(0.5)
26
+ end
27
+
28
+ it 'defines IMPORTANCE_WEIGHTS with four levels' do
29
+ weights = described_class::IMPORTANCE_WEIGHTS
30
+ expect(weights[:core]).to eq(1.0)
31
+ expect(weights[:significant]).to eq(0.7)
32
+ expect(weights[:moderate]).to eq(0.5)
33
+ expect(weights[:peripheral]).to eq(0.25)
34
+ end
35
+
36
+ it 'defines RESOLUTION_STRATEGIES' do
37
+ expect(described_class::RESOLUTION_STRATEGIES).to contain_exactly(:belief_revision, :rationalization, :avoidance)
38
+ end
39
+
40
+ it 'defines STRESS_CEILING and STRESS_FLOOR' do
41
+ expect(described_class::STRESS_CEILING).to eq(1.0)
42
+ expect(described_class::STRESS_FLOOR).to eq(0.0)
43
+ end
44
+
45
+ it 'defines CONTRADICTION_TYPES' do
46
+ expect(described_class::CONTRADICTION_TYPES).to contain_exactly(:direct, :inverse, :conditional, :temporal)
47
+ end
48
+
49
+ it 'freezes IMPORTANCE_WEIGHTS' do
50
+ expect(described_class::IMPORTANCE_WEIGHTS).to be_frozen
51
+ end
52
+
53
+ it 'freezes RESOLUTION_STRATEGIES' do
54
+ expect(described_class::RESOLUTION_STRATEGIES).to be_frozen
55
+ end
56
+
57
+ it 'freezes CONTRADICTION_TYPES' do
58
+ expect(described_class::CONTRADICTION_TYPES).to be_frozen
59
+ end
60
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Dissonance::Helpers::DissonanceEvent do
4
+ let(:event) do
5
+ described_class.new(
6
+ belief_a_id: 'uuid-a',
7
+ belief_b_id: 'uuid-b',
8
+ domain: 'ethics',
9
+ magnitude: 0.6,
10
+ contradiction_type: :direct
11
+ )
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'assigns a uuid id' do
16
+ expect(event.id).to match(/\A[0-9a-f-]{36}\z/)
17
+ end
18
+
19
+ it 'assigns belief_a_id' do
20
+ expect(event.belief_a_id).to eq('uuid-a')
21
+ end
22
+
23
+ it 'assigns belief_b_id' do
24
+ expect(event.belief_b_id).to eq('uuid-b')
25
+ end
26
+
27
+ it 'assigns domain' do
28
+ expect(event.domain).to eq('ethics')
29
+ end
30
+
31
+ it 'assigns magnitude' do
32
+ expect(event.magnitude).to eq(0.6)
33
+ end
34
+
35
+ it 'assigns contradiction_type' do
36
+ expect(event.contradiction_type).to eq(:direct)
37
+ end
38
+
39
+ it 'starts as unresolved' do
40
+ expect(event.resolved).to be false
41
+ end
42
+
43
+ it 'starts with nil resolution_strategy' do
44
+ expect(event.resolution_strategy).to be_nil
45
+ end
46
+
47
+ it 'assigns timestamp as UTC Time' do
48
+ expect(event.timestamp).to be_a(Time)
49
+ end
50
+
51
+ it 'clamps magnitude above 1.0' do
52
+ ev = described_class.new(belief_a_id: 'a', belief_b_id: 'b', domain: 'd', magnitude: 1.5)
53
+ expect(ev.magnitude).to eq(1.0)
54
+ end
55
+
56
+ it 'clamps magnitude below 0.0' do
57
+ ev = described_class.new(belief_a_id: 'a', belief_b_id: 'b', domain: 'd', magnitude: -0.5)
58
+ expect(ev.magnitude).to eq(0.0)
59
+ end
60
+
61
+ it 'defaults contradiction_type to :direct' do
62
+ ev = described_class.new(belief_a_id: 'a', belief_b_id: 'b', domain: 'd', magnitude: 0.5)
63
+ expect(ev.contradiction_type).to eq(:direct)
64
+ end
65
+ end
66
+
67
+ describe '#resolve!' do
68
+ it 'marks the event as resolved' do
69
+ event.resolve!(:belief_revision)
70
+ expect(event.resolved).to be true
71
+ end
72
+
73
+ it 'records the resolution strategy' do
74
+ event.resolve!(:rationalization)
75
+ expect(event.resolution_strategy).to eq(:rationalization)
76
+ end
77
+
78
+ it 'returns self for chaining' do
79
+ result = event.resolve!(:avoidance)
80
+ expect(result).to be(event)
81
+ end
82
+
83
+ it 'can be resolved with any valid strategy' do
84
+ Legion::Extensions::Dissonance::Helpers::Constants::RESOLUTION_STRATEGIES.each do |strategy|
85
+ ev = described_class.new(belief_a_id: 'a', belief_b_id: 'b', domain: 'd', magnitude: 0.5)
86
+ ev.resolve!(strategy)
87
+ expect(ev.resolution_strategy).to eq(strategy)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe '#to_h' do
93
+ it 'returns hash with all fields when unresolved' do
94
+ h = event.to_h
95
+ expect(h[:id]).to eq(event.id)
96
+ expect(h[:belief_a_id]).to eq('uuid-a')
97
+ expect(h[:belief_b_id]).to eq('uuid-b')
98
+ expect(h[:domain]).to eq('ethics')
99
+ expect(h[:magnitude]).to eq(0.6)
100
+ expect(h[:contradiction_type]).to eq(:direct)
101
+ expect(h[:resolved]).to be false
102
+ expect(h[:resolution_strategy]).to be_nil
103
+ expect(h[:timestamp]).to be_a(Time)
104
+ end
105
+
106
+ it 'reflects resolved state in to_h' do
107
+ event.resolve!(:belief_revision)
108
+ h = event.to_h
109
+ expect(h[:resolved]).to be true
110
+ expect(h[:resolution_strategy]).to eq(:belief_revision)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Dissonance::Helpers::DissonanceModel do
4
+ subject(:model) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts with empty beliefs' do
8
+ expect(model.beliefs).to be_empty
9
+ end
10
+
11
+ it 'starts with empty events' do
12
+ expect(model.events).to be_empty
13
+ end
14
+
15
+ it 'starts with zero stress' do
16
+ expect(model.stress).to eq(0.0)
17
+ end
18
+ end
19
+
20
+ describe '#add_belief' do
21
+ it 'adds a belief to the model' do
22
+ model.add_belief(domain: 'ethics', content: 'honesty matters', confidence: 0.8, importance: :core)
23
+ expect(model.beliefs.size).to eq(1)
24
+ end
25
+
26
+ it 'returns the new belief and empty events for first belief in domain' do
27
+ result = model.add_belief(domain: 'ethics', content: 'honesty matters')
28
+ expect(result[:belief]).to be_a(Legion::Extensions::Dissonance::Helpers::Belief)
29
+ expect(result[:new_dissonance_events]).to be_empty
30
+ end
31
+
32
+ it 'detects contradiction between beliefs in the same domain' do
33
+ model.add_belief(domain: 'safety', content: 'always ask permission')
34
+ result = model.add_belief(domain: 'safety', content: 'act autonomously')
35
+ expect(result[:new_dissonance_events].size).to eq(1)
36
+ end
37
+
38
+ it 'does not detect contradiction for same content' do
39
+ model.add_belief(domain: 'safety', content: 'always ask permission')
40
+ result = model.add_belief(domain: 'safety', content: 'always ask permission')
41
+ expect(result[:new_dissonance_events]).to be_empty
42
+ end
43
+
44
+ it 'does not create duplicate contradiction events for the same pair' do
45
+ model.add_belief(domain: 'safety', content: 'always ask permission')
46
+ result1 = model.add_belief(domain: 'safety', content: 'act autonomously')
47
+ ev_id = result1[:new_dissonance_events].first.id
48
+ # Artificially call detect_contradictions again — no new events since pair is tracked
49
+ new_evs = model.detect_contradictions
50
+ expect(new_evs).to be_empty
51
+ expect(model.events.size).to eq(1)
52
+ expect(model.events.key?(ev_id)).to be true
53
+ end
54
+
55
+ it 'does not create contradiction across different domains' do
56
+ model.add_belief(domain: 'ethics', content: 'be transparent')
57
+ result = model.add_belief(domain: 'safety', content: 'act differently')
58
+ expect(result[:new_dissonance_events]).to be_empty
59
+ end
60
+
61
+ it 'stores the belief with the correct importance weight' do
62
+ result = model.add_belief(domain: 'ethics', content: 'test', importance: :significant)
63
+ expect(result[:belief].importance).to eq(:significant)
64
+ end
65
+ end
66
+
67
+ describe '#detect_contradictions' do
68
+ it 'returns empty array when no contradictions exist' do
69
+ model.add_belief(domain: 'ethics', content: 'honesty matters')
70
+ expect(model.detect_contradictions).to be_empty
71
+ end
72
+
73
+ it 'finds untracked contradictions among existing beliefs' do
74
+ b1 = Legion::Extensions::Dissonance::Helpers::Belief.new(domain: 'ethics', content: 'be honest')
75
+ b2 = Legion::Extensions::Dissonance::Helpers::Belief.new(domain: 'ethics', content: 'deceive when needed')
76
+ model.beliefs[b1.id] = b1
77
+ model.beliefs[b2.id] = b2
78
+ new_events = model.detect_contradictions
79
+ expect(new_events.size).to eq(1)
80
+ end
81
+
82
+ it 'does not re-detect already tracked contradictions' do
83
+ model.add_belief(domain: 'ethics', content: 'tell truth')
84
+ model.add_belief(domain: 'ethics', content: 'lie sometimes')
85
+ model.detect_contradictions
86
+ second_run = model.detect_contradictions
87
+ expect(second_run).to be_empty
88
+ end
89
+ end
90
+
91
+ describe '#resolve' do
92
+ let(:event_id) do
93
+ model.add_belief(domain: 'ethics', content: 'be honest')
94
+ result = model.add_belief(domain: 'ethics', content: 'deceive when useful')
95
+ result[:new_dissonance_events].first.id
96
+ end
97
+
98
+ it 'returns the resolved event' do
99
+ event = model.resolve(event_id, strategy: :belief_revision)
100
+ expect(event).to be_a(Legion::Extensions::Dissonance::Helpers::DissonanceEvent)
101
+ expect(event.resolved).to be true
102
+ end
103
+
104
+ it 'reduces stress after belief_revision resolution' do
105
+ model.add_belief(domain: 'ethics', content: 'be honest')
106
+ result = model.add_belief(domain: 'ethics', content: 'deceive when useful')
107
+ ev_id = result[:new_dissonance_events].first.id
108
+ model.decay
109
+ stress_before = model.stress
110
+ model.resolve(ev_id, strategy: :belief_revision)
111
+ expect(model.stress).to be < stress_before
112
+ end
113
+
114
+ it 'reduces stress less with rationalization than belief_revision' do
115
+ model2 = described_class.new
116
+ model.add_belief(domain: 'domain', content: 'claim a')
117
+ ev_id_m1 = model.add_belief(domain: 'domain', content: 'claim b')[:new_dissonance_events].first.id
118
+ model2.add_belief(domain: 'domain', content: 'claim a')
119
+ ev_id_m2 = model2.add_belief(domain: 'domain', content: 'claim b')[:new_dissonance_events].first.id
120
+ # Build up enough stress so that relief amounts differ meaningfully after clamping
121
+ 15.times { model.decay }
122
+ 15.times { model2.decay }
123
+ model.resolve(ev_id_m1, strategy: :belief_revision)
124
+ model2.resolve(ev_id_m2, strategy: :rationalization)
125
+ expect(model.stress).to be < model2.stress
126
+ end
127
+
128
+ it 'returns nil for unknown event_id' do
129
+ expect(model.resolve('non-existent-id', strategy: :belief_revision)).to be_nil
130
+ end
131
+
132
+ it 'returns nil if already resolved' do
133
+ model.add_belief(domain: 'ethics', content: 'be honest')
134
+ result = model.add_belief(domain: 'ethics', content: 'deceive when useful')
135
+ ev_id = result[:new_dissonance_events].first.id
136
+ model.resolve(ev_id, strategy: :belief_revision)
137
+ expect(model.resolve(ev_id, strategy: :rationalization)).to be_nil
138
+ end
139
+
140
+ it 'returns nil for invalid strategy' do
141
+ model.add_belief(domain: 'x', content: 'a')
142
+ result = model.add_belief(domain: 'x', content: 'b')
143
+ ev_id = result[:new_dissonance_events].first.id
144
+ expect(model.resolve(ev_id, strategy: :invalid_strategy)).to be_nil
145
+ end
146
+ end
147
+
148
+ describe '#stress_level' do
149
+ it 'returns 0.0 initially' do
150
+ expect(model.stress_level).to eq(0.0)
151
+ end
152
+
153
+ it 'increases after decay with unresolved events' do
154
+ model.add_belief(domain: 'x', content: 'a')
155
+ model.add_belief(domain: 'x', content: 'b')
156
+ model.decay
157
+ expect(model.stress_level).to be > 0.0
158
+ end
159
+ end
160
+
161
+ describe '#domain_stress' do
162
+ it 'returns 0.0 for a domain with no unresolved events' do
163
+ expect(model.domain_stress('ethics')).to eq(0.0)
164
+ end
165
+
166
+ it 'returns positive stress for domain with unresolved events' do
167
+ model.add_belief(domain: 'ethics', content: 'be honest')
168
+ model.add_belief(domain: 'ethics', content: 'hide truth')
169
+ expect(model.domain_stress('ethics')).to be > 0.0
170
+ end
171
+
172
+ it 'returns 0.0 for a domain after all events resolved' do
173
+ model.add_belief(domain: 'ethics', content: 'be honest')
174
+ result = model.add_belief(domain: 'ethics', content: 'hide truth')
175
+ ev_id = result[:new_dissonance_events].first.id
176
+ model.resolve(ev_id, strategy: :belief_revision)
177
+ expect(model.domain_stress('ethics')).to eq(0.0)
178
+ end
179
+ end
180
+
181
+ describe '#unresolved_events' do
182
+ it 'returns empty list when no events' do
183
+ expect(model.unresolved_events).to be_empty
184
+ end
185
+
186
+ it 'returns only unresolved events' do
187
+ model.add_belief(domain: 'x', content: 'a')
188
+ result = model.add_belief(domain: 'x', content: 'b')
189
+ ev_id = result[:new_dissonance_events].first.id
190
+ model.resolve(ev_id, strategy: :belief_revision)
191
+
192
+ model.add_belief(domain: 'y', content: 'p')
193
+ model.add_belief(domain: 'y', content: 'q')
194
+
195
+ unresolved = model.unresolved_events
196
+ expect(unresolved.all? { |ev| !ev.resolved }).to be true
197
+ end
198
+ end
199
+
200
+ describe '#decay' do
201
+ it 'increases stress when there are unresolved events' do
202
+ model.add_belief(domain: 'x', content: 'a')
203
+ model.add_belief(domain: 'x', content: 'b')
204
+ initial_stress = model.stress
205
+ model.decay
206
+ expect(model.stress).to be > initial_stress
207
+ end
208
+
209
+ it 'decreases stress when there are no unresolved events' do
210
+ model.instance_variable_set(:@stress, 0.5)
211
+ model.decay
212
+ expect(model.stress).to be < 0.5
213
+ end
214
+
215
+ it 'does not exceed STRESS_CEILING' do
216
+ model.instance_variable_set(:@stress, 0.99)
217
+ 100.times { model.add_belief(domain: 'x', content: "belief_#{rand}") }
218
+ model.decay
219
+ expect(model.stress).to be <= 1.0
220
+ end
221
+
222
+ it 'does not go below STRESS_FLOOR' do
223
+ model.decay
224
+ expect(model.stress).to be >= 0.0
225
+ end
226
+
227
+ it 'returns the current stress value' do
228
+ result = model.decay
229
+ expect(result).to eq(model.stress)
230
+ end
231
+ end
232
+
233
+ describe '#to_h' do
234
+ it 'returns a snapshot hash' do
235
+ model.add_belief(domain: 'ethics', content: 'test')
236
+ h = model.to_h
237
+ expect(h[:beliefs]).to be_an(Array)
238
+ expect(h[:events]).to be_an(Array)
239
+ expect(h[:stress]).to eq(model.stress)
240
+ expect(h[:total_beliefs]).to eq(1)
241
+ expect(h[:total_events]).to eq(0)
242
+ expect(h[:unresolved_count]).to eq(0)
243
+ end
244
+
245
+ it 'counts unresolved events correctly' do
246
+ model.add_belief(domain: 'x', content: 'a')
247
+ model.add_belief(domain: 'x', content: 'b')
248
+ h = model.to_h
249
+ expect(h[:unresolved_count]).to eq(1)
250
+ end
251
+ end
252
+ end