lex-cognitive-synthesis 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.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/lex-cognitive-synthesis.gemspec +31 -0
- data/lib/legion/extensions/cognitive_synthesis/client.rb +25 -0
- data/lib/legion/extensions/cognitive_synthesis/helpers/constants.rb +37 -0
- data/lib/legion/extensions/cognitive_synthesis/helpers/synthesis.rb +59 -0
- data/lib/legion/extensions/cognitive_synthesis/helpers/synthesis_engine.rb +209 -0
- data/lib/legion/extensions/cognitive_synthesis/helpers/synthesis_stream.rb +63 -0
- data/lib/legion/extensions/cognitive_synthesis/runners/cognitive_synthesis.rb +78 -0
- data/lib/legion/extensions/cognitive_synthesis/version.rb +9 -0
- data/lib/legion/extensions/cognitive_synthesis.rb +16 -0
- data/spec/legion/extensions/cognitive_synthesis/client_spec.rb +26 -0
- data/spec/legion/extensions/cognitive_synthesis/helpers/constants_spec.rb +87 -0
- data/spec/legion/extensions/cognitive_synthesis/helpers/synthesis_engine_spec.rb +233 -0
- data/spec/legion/extensions/cognitive_synthesis/helpers/synthesis_spec.rb +101 -0
- data/spec/legion/extensions/cognitive_synthesis/helpers/synthesis_stream_spec.rb +116 -0
- data/spec/legion/extensions/cognitive_synthesis/runners/cognitive_synthesis_spec.rb +181 -0
- data/spec/spec_helper.rb +20 -0
- metadata +79 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveSynthesis::Helpers::SynthesisEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
def add_stream(type: :emotional, content: { value: 1 }, weight: 0.7, confidence: 0.8)
|
|
7
|
+
engine.add_stream(stream_type: type, content: content, weight: weight, confidence: confidence)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#add_stream' do
|
|
11
|
+
it 'returns success with stream_id' do
|
|
12
|
+
result = add_stream
|
|
13
|
+
expect(result[:success]).to be true
|
|
14
|
+
expect(result[:stream_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'stores stream in @streams' do
|
|
18
|
+
add_stream
|
|
19
|
+
expect(engine.streams.size).to eq(1)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'rejects invalid stream_type' do
|
|
23
|
+
result = engine.add_stream(stream_type: :invalid, content: {})
|
|
24
|
+
expect(result[:success]).to be false
|
|
25
|
+
expect(result[:error]).to eq(:invalid_stream_type)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'accepts all valid stream types' do
|
|
29
|
+
%i[emotional perceptual memorial predictive reasoning social identity motor].each do |type|
|
|
30
|
+
result = engine.add_stream(stream_type: type, content: {})
|
|
31
|
+
expect(result[:success]).to be true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'prunes when exceeding MAX_STREAMS' do
|
|
36
|
+
51.times { add_stream }
|
|
37
|
+
expect(engine.streams.size).to eq(50)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#remove_stream' do
|
|
42
|
+
it 'removes an existing stream' do
|
|
43
|
+
result = add_stream
|
|
44
|
+
remove = engine.remove_stream(stream_id: result[:stream_id])
|
|
45
|
+
expect(remove[:success]).to be true
|
|
46
|
+
expect(engine.streams).to be_empty
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns not_found for missing stream' do
|
|
50
|
+
result = engine.remove_stream(stream_id: 'nonexistent')
|
|
51
|
+
expect(result[:success]).to be false
|
|
52
|
+
expect(result[:error]).to eq(:not_found)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#synthesize!' do
|
|
57
|
+
it 'fails with fewer than MIN_STREAMS_FOR_SYNTHESIS active streams' do
|
|
58
|
+
add_stream
|
|
59
|
+
result = engine.synthesize!
|
|
60
|
+
expect(result[:success]).to be false
|
|
61
|
+
expect(result[:error]).to eq(:insufficient_streams)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'succeeds with enough streams' do
|
|
65
|
+
2.times { add_stream }
|
|
66
|
+
result = engine.synthesize!
|
|
67
|
+
expect(result[:success]).to be true
|
|
68
|
+
expect(result[:synthesis]).to have_key(:id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'stores synthesis in history' do
|
|
72
|
+
2.times { add_stream }
|
|
73
|
+
engine.synthesize!
|
|
74
|
+
expect(engine.syntheses.size).to eq(1)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'includes coherence in result' do
|
|
78
|
+
2.times { add_stream }
|
|
79
|
+
result = engine.synthesize!
|
|
80
|
+
expect(result[:synthesis][:coherence]).to be_between(0.0, 1.0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'includes novelty in result' do
|
|
84
|
+
2.times { add_stream }
|
|
85
|
+
result = engine.synthesize!
|
|
86
|
+
expect(result[:synthesis][:novelty]).to be_between(0.0, 1.0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'includes confidence in result' do
|
|
90
|
+
2.times { add_stream }
|
|
91
|
+
result = engine.synthesize!
|
|
92
|
+
expect(result[:synthesis][:confidence]).to be_between(0.0, 1.0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'marks first synthesis as maximally novel (no prior)' do
|
|
96
|
+
2.times { add_stream }
|
|
97
|
+
result = engine.synthesize!
|
|
98
|
+
expect(result[:synthesis][:novelty]).to eq(1.0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'caps syntheses at MAX_SYNTHESES' do
|
|
102
|
+
2.times { add_stream }
|
|
103
|
+
201.times { engine.synthesize! }
|
|
104
|
+
expect(engine.syntheses.size).to eq(200)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'returns content with dominant_type' do
|
|
108
|
+
2.times { add_stream }
|
|
109
|
+
result = engine.synthesize!
|
|
110
|
+
expect(result[:synthesis][:content]).to have_key(:dominant_type)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#decay_all!' do
|
|
115
|
+
it 'returns success' do
|
|
116
|
+
result = engine.decay_all!
|
|
117
|
+
expect(result[:success]).to be true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'reduces freshness on all streams' do
|
|
121
|
+
add_stream
|
|
122
|
+
stream = engine.streams.values.first
|
|
123
|
+
before = stream.freshness
|
|
124
|
+
engine.decay_all!
|
|
125
|
+
expect(stream.freshness).to be < before
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'removes stale streams' do
|
|
129
|
+
add_stream
|
|
130
|
+
stream = engine.streams.values.first
|
|
131
|
+
46.times { stream.decay_freshness! }
|
|
132
|
+
engine.decay_all!
|
|
133
|
+
expect(engine.streams).to be_empty
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'reports removed and remaining counts' do
|
|
137
|
+
2.times { add_stream }
|
|
138
|
+
result = engine.decay_all!
|
|
139
|
+
expect(result).to have_key(:streams_removed)
|
|
140
|
+
expect(result).to have_key(:streams_remaining)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '#stream_conflict?' do
|
|
145
|
+
let(:id_a) { add_stream(weight: 0.9, content: { signal: :danger })[:stream_id] }
|
|
146
|
+
let(:id_b) { add_stream(weight: 0.1, content: { signal: :safe })[:stream_id] }
|
|
147
|
+
|
|
148
|
+
it 'detects weight opposition' do
|
|
149
|
+
result = engine.stream_conflict?(stream_id_a: id_a, stream_id_b: id_b)
|
|
150
|
+
expect(result[:success]).to be true
|
|
151
|
+
expect(result[:weight_opposition]).to be true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'detects content conflict on shared keys' do
|
|
155
|
+
result = engine.stream_conflict?(stream_id_a: id_a, stream_id_b: id_b)
|
|
156
|
+
expect(result[:content_conflict]).to be true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'reports no conflict for compatible streams' do
|
|
160
|
+
a = add_stream(weight: 0.6, content: { value: 1 })[:stream_id]
|
|
161
|
+
b = add_stream(weight: 0.7, content: { value: 1 })[:stream_id]
|
|
162
|
+
result = engine.stream_conflict?(stream_id_a: a, stream_id_b: b)
|
|
163
|
+
expect(result[:conflict]).to be false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'returns not_found for missing stream' do
|
|
167
|
+
result = engine.stream_conflict?(stream_id_a: 'x', stream_id_b: 'y')
|
|
168
|
+
expect(result[:success]).to be false
|
|
169
|
+
expect(result[:error]).to eq(:stream_not_found)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe '#dominant_stream' do
|
|
174
|
+
it 'returns no_streams error when empty' do
|
|
175
|
+
result = engine.dominant_stream
|
|
176
|
+
expect(result[:success]).to be false
|
|
177
|
+
expect(result[:error]).to eq(:no_streams)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'returns the stream with highest effective_weight' do
|
|
181
|
+
add_stream(weight: 0.3, confidence: 0.3)
|
|
182
|
+
id = add_stream(weight: 0.9, confidence: 0.95)[:stream_id]
|
|
183
|
+
result = engine.dominant_stream
|
|
184
|
+
expect(result[:success]).to be true
|
|
185
|
+
expect(result[:stream][:id]).to eq(id)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe '#synthesis_history' do
|
|
190
|
+
it 'returns empty list when no syntheses' do
|
|
191
|
+
result = engine.synthesis_history
|
|
192
|
+
expect(result[:syntheses]).to be_empty
|
|
193
|
+
expect(result[:count]).to eq(0)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'returns last N syntheses' do
|
|
197
|
+
2.times { add_stream }
|
|
198
|
+
5.times { engine.synthesize! }
|
|
199
|
+
result = engine.synthesis_history(limit: 3)
|
|
200
|
+
expect(result[:count]).to eq(3)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
describe '#average_coherence' do
|
|
205
|
+
it 'returns 0.0 when no syntheses' do
|
|
206
|
+
result = engine.average_coherence
|
|
207
|
+
expect(result[:average_coherence]).to eq(0.0)
|
|
208
|
+
expect(result[:sample_size]).to eq(0)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'computes average coherence over recent syntheses' do
|
|
212
|
+
2.times { add_stream }
|
|
213
|
+
3.times { engine.synthesize! }
|
|
214
|
+
result = engine.average_coherence
|
|
215
|
+
expect(result[:average_coherence]).to be_between(0.0, 1.0)
|
|
216
|
+
expect(result[:sample_size]).to eq(3)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
describe '#to_h' do
|
|
221
|
+
it 'returns summary stats hash' do
|
|
222
|
+
add_stream(type: :emotional)
|
|
223
|
+
result = engine.to_h
|
|
224
|
+
expect(result).to include(:stream_count, :synthesis_count, :active_streams,
|
|
225
|
+
:stale_streams, :average_coherence)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'reflects current stream count' do
|
|
229
|
+
3.times { add_stream }
|
|
230
|
+
expect(engine.to_h[:stream_count]).to eq(3)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveSynthesis::Helpers::Synthesis do
|
|
4
|
+
subject(:synthesis) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
streams: %w[abc def],
|
|
7
|
+
coherence: 0.75,
|
|
8
|
+
novelty: 0.8,
|
|
9
|
+
confidence: 0.7,
|
|
10
|
+
content: { dominant_type: :emotional, stream_count: 2 }
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'assigns a UUID id' do
|
|
16
|
+
expect(synthesis.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'stores stream ids' do
|
|
20
|
+
expect(synthesis.streams).to eq(%w[abc def])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'clamps coherence to 0-1' do
|
|
24
|
+
s = described_class.new(streams: [], coherence: 1.5, novelty: 0.5, confidence: 0.5, content: {})
|
|
25
|
+
expect(s.coherence).to eq(1.0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'clamps novelty to 0-1' do
|
|
29
|
+
s = described_class.new(streams: [], coherence: 0.5, novelty: -0.5, confidence: 0.5, content: {})
|
|
30
|
+
expect(s.novelty).to eq(0.0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'clamps confidence to 0-1' do
|
|
34
|
+
s = described_class.new(streams: [], coherence: 0.5, novelty: 0.5, confidence: 2.0, content: {})
|
|
35
|
+
expect(s.confidence).to eq(1.0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'sets created_at as UTC time' do
|
|
39
|
+
expect(synthesis.created_at).to be_a(Time)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#fragmented?' do
|
|
44
|
+
it 'returns false when coherence >= COHERENCE_THRESHOLD' do
|
|
45
|
+
expect(synthesis.fragmented?).to be false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns true when coherence < COHERENCE_THRESHOLD' do
|
|
49
|
+
s = described_class.new(streams: [], coherence: 0.4, novelty: 0.5, confidence: 0.5, content: {})
|
|
50
|
+
expect(s.fragmented?).to be true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#novel?' do
|
|
55
|
+
it 'returns true when novelty > NOVELTY_THRESHOLD' do
|
|
56
|
+
expect(synthesis.novel?).to be true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'returns false when novelty <= NOVELTY_THRESHOLD' do
|
|
60
|
+
s = described_class.new(streams: [], coherence: 0.5, novelty: 0.5, confidence: 0.5, content: {})
|
|
61
|
+
expect(s.novel?).to be false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#coherence_label' do
|
|
66
|
+
it 'returns :coherent for coherence 0.75' do
|
|
67
|
+
expect(synthesis.coherence_label).to eq(:coherent)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'returns :unified for coherence 0.9' do
|
|
71
|
+
s = described_class.new(streams: [], coherence: 0.9, novelty: 0.5, confidence: 0.5, content: {})
|
|
72
|
+
expect(s.coherence_label).to eq(:unified)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#confidence_label' do
|
|
77
|
+
it 'returns :confident for confidence 0.7' do
|
|
78
|
+
expect(synthesis.confidence_label).to eq(:confident)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#to_h' do
|
|
83
|
+
let(:hash) { synthesis.to_h }
|
|
84
|
+
|
|
85
|
+
it 'includes all expected keys' do
|
|
86
|
+
%i[id streams coherence novelty confidence content fragmented novel
|
|
87
|
+
coherence_label confidence_label created_at].each do |key|
|
|
88
|
+
expect(hash).to have_key(key)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'reflects fragmented state' do
|
|
93
|
+
s = described_class.new(streams: [], coherence: 0.3, novelty: 0.5, confidence: 0.5, content: {})
|
|
94
|
+
expect(s.to_h[:fragmented]).to be true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'reflects novel state' do
|
|
98
|
+
expect(hash[:novel]).to be true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveSynthesis::Helpers::SynthesisStream do
|
|
4
|
+
subject(:stream) do
|
|
5
|
+
described_class.new(stream_type: :emotional, content: { mood: 'alert' }, weight: 0.8, confidence: 0.9)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'assigns a UUID id' do
|
|
10
|
+
expect(stream.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'sets stream_type' do
|
|
14
|
+
expect(stream.stream_type).to eq(:emotional)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'sets content' do
|
|
18
|
+
expect(stream.content).to eq({ mood: 'alert' })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'clamps weight to 0-1' do
|
|
22
|
+
s = described_class.new(stream_type: :perceptual, content: {}, weight: 1.5)
|
|
23
|
+
expect(s.weight).to eq(1.0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'clamps confidence to 0-1' do
|
|
27
|
+
s = described_class.new(stream_type: :perceptual, content: {}, confidence: -0.1)
|
|
28
|
+
expect(s.confidence).to eq(0.0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'initializes freshness at 1.0' do
|
|
32
|
+
expect(stream.freshness).to eq(1.0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'sets created_at as UTC time' do
|
|
36
|
+
expect(stream.created_at).to be_a(Time)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#decay_freshness!' do
|
|
41
|
+
it 'reduces freshness by FRESHNESS_DECAY' do
|
|
42
|
+
before = stream.freshness
|
|
43
|
+
stream.decay_freshness!
|
|
44
|
+
expect(stream.freshness).to be_within(0.001).of(before - 0.02)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'never drops below 0.0' do
|
|
48
|
+
60.times { stream.decay_freshness! }
|
|
49
|
+
expect(stream.freshness).to eq(0.0)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#stale?' do
|
|
54
|
+
it 'returns false when freshness is high' do
|
|
55
|
+
expect(stream.stale?).to be false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns true when freshness drops below 0.1' do
|
|
59
|
+
46.times { stream.decay_freshness! }
|
|
60
|
+
expect(stream.stale?).to be true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#effective_weight' do
|
|
65
|
+
it 'equals weight * freshness * confidence' do
|
|
66
|
+
expected = (0.8 * 1.0 * 0.9).round(10)
|
|
67
|
+
expect(stream.effective_weight).to eq(expected)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'decreases after decay' do
|
|
71
|
+
before = stream.effective_weight
|
|
72
|
+
stream.decay_freshness!
|
|
73
|
+
expect(stream.effective_weight).to be < before
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#coherence_label' do
|
|
78
|
+
it 'returns :unified for weight 0.9' do
|
|
79
|
+
s = described_class.new(stream_type: :memorial, content: {}, weight: 0.9)
|
|
80
|
+
expect(s.coherence_label).to eq(:unified)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'returns :chaotic for weight 0.1' do
|
|
84
|
+
s = described_class.new(stream_type: :memorial, content: {}, weight: 0.1)
|
|
85
|
+
expect(s.coherence_label).to eq(:chaotic)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#confidence_label' do
|
|
90
|
+
it 'returns :certain for confidence 0.95' do
|
|
91
|
+
s = described_class.new(stream_type: :memorial, content: {}, confidence: 0.95)
|
|
92
|
+
expect(s.confidence_label).to eq(:certain)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'returns :guessing for confidence 0.1' do
|
|
96
|
+
s = described_class.new(stream_type: :memorial, content: {}, confidence: 0.1)
|
|
97
|
+
expect(s.confidence_label).to eq(:guessing)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#to_h' do
|
|
102
|
+
let(:hash) { stream.to_h }
|
|
103
|
+
|
|
104
|
+
it 'includes all expected keys' do
|
|
105
|
+
%i[id stream_type content weight confidence freshness effective_weight
|
|
106
|
+
stale coherence_label confidence_label created_at].each do |key|
|
|
107
|
+
expect(hash).to have_key(key)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'reflects current freshness' do
|
|
112
|
+
stream.decay_freshness!
|
|
113
|
+
expect(stream.to_h[:freshness]).to be < 1.0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_synthesis/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveSynthesis::Runners::CognitiveSynthesis do
|
|
6
|
+
let(:client) { Legion::Extensions::CognitiveSynthesis::Client.new }
|
|
7
|
+
|
|
8
|
+
def add_two_streams
|
|
9
|
+
client.add_stream(stream_type: :emotional, content: { mood: :alert }, weight: 0.8, confidence: 0.9)
|
|
10
|
+
client.add_stream(stream_type: :perceptual, content: { threat: :detected }, weight: 0.6, confidence: 0.7)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#add_stream' do
|
|
14
|
+
it 'adds a valid stream and returns success' do
|
|
15
|
+
result = client.add_stream(stream_type: :emotional, content: { mood: :calm })
|
|
16
|
+
expect(result[:success]).to be true
|
|
17
|
+
expect(result[:stream_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'rejects an invalid stream type' do
|
|
21
|
+
result = client.add_stream(stream_type: :bogus, content: {})
|
|
22
|
+
expect(result[:success]).to be false
|
|
23
|
+
expect(result[:error]).to eq(:invalid_stream_type)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'accepts all valid stream types' do
|
|
27
|
+
%i[emotional perceptual memorial predictive reasoning social identity motor].each do |type|
|
|
28
|
+
result = client.add_stream(stream_type: type, content: {})
|
|
29
|
+
expect(result[:success]).to be true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'accepts injected engine kwarg' do
|
|
34
|
+
engine = Legion::Extensions::CognitiveSynthesis::Helpers::SynthesisEngine.new
|
|
35
|
+
result = client.add_stream(stream_type: :reasoning, content: {}, engine: engine)
|
|
36
|
+
expect(result[:success]).to be true
|
|
37
|
+
expect(engine.streams.size).to eq(1)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#remove_stream' do
|
|
42
|
+
it 'removes an existing stream' do
|
|
43
|
+
add_result = client.add_stream(stream_type: :emotional, content: {})
|
|
44
|
+
remove_result = client.remove_stream(stream_id: add_result[:stream_id])
|
|
45
|
+
expect(remove_result[:success]).to be true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns not_found for unknown stream' do
|
|
49
|
+
result = client.remove_stream(stream_id: 'nonexistent-id')
|
|
50
|
+
expect(result[:success]).to be false
|
|
51
|
+
expect(result[:error]).to eq(:not_found)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#synthesize' do
|
|
56
|
+
it 'returns insufficient_streams when fewer than 2 active streams' do
|
|
57
|
+
client.add_stream(stream_type: :emotional, content: {})
|
|
58
|
+
result = client.synthesize
|
|
59
|
+
expect(result[:success]).to be false
|
|
60
|
+
expect(result[:error]).to eq(:insufficient_streams)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'succeeds with 2+ active streams' do
|
|
64
|
+
add_two_streams
|
|
65
|
+
result = client.synthesize
|
|
66
|
+
expect(result[:success]).to be true
|
|
67
|
+
expect(result[:synthesis]).to have_key(:id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'returns coherence between 0 and 1' do
|
|
71
|
+
add_two_streams
|
|
72
|
+
result = client.synthesize
|
|
73
|
+
expect(result[:synthesis][:coherence]).to be_between(0.0, 1.0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns novelty = 1.0 for first synthesis' do
|
|
77
|
+
add_two_streams
|
|
78
|
+
result = client.synthesize
|
|
79
|
+
expect(result[:synthesis][:novelty]).to eq(1.0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'uses injected engine when provided' do
|
|
83
|
+
engine = Legion::Extensions::CognitiveSynthesis::Helpers::SynthesisEngine.new
|
|
84
|
+
2.times { engine.add_stream(stream_type: :emotional, content: {}) }
|
|
85
|
+
result = client.synthesize(engine: engine)
|
|
86
|
+
expect(result[:success]).to be true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#decay_streams' do
|
|
91
|
+
it 'returns success' do
|
|
92
|
+
result = client.decay_streams
|
|
93
|
+
expect(result[:success]).to be true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'reports removed count' do
|
|
97
|
+
result = client.decay_streams
|
|
98
|
+
expect(result).to have_key(:streams_removed)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#check_conflict' do
|
|
103
|
+
it 'detects conflict between opposing streams' do
|
|
104
|
+
a = client.add_stream(stream_type: :emotional, content: { signal: :danger }, weight: 0.9)
|
|
105
|
+
b = client.add_stream(stream_type: :perceptual, content: { signal: :safe }, weight: 0.1)
|
|
106
|
+
result = client.check_conflict(stream_id_a: a[:stream_id], stream_id_b: b[:stream_id])
|
|
107
|
+
expect(result[:success]).to be true
|
|
108
|
+
expect(result[:conflict]).to be true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'reports no conflict for similar streams' do
|
|
112
|
+
a = client.add_stream(stream_type: :emotional, content: { value: 1 }, weight: 0.6)
|
|
113
|
+
b = client.add_stream(stream_type: :perceptual, content: { value: 1 }, weight: 0.65)
|
|
114
|
+
result = client.check_conflict(stream_id_a: a[:stream_id], stream_id_b: b[:stream_id])
|
|
115
|
+
expect(result[:conflict]).to be false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'returns error for missing stream ids' do
|
|
119
|
+
result = client.check_conflict(stream_id_a: 'x', stream_id_b: 'y')
|
|
120
|
+
expect(result[:success]).to be false
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe '#dominant_stream' do
|
|
125
|
+
it 'returns error when no streams present' do
|
|
126
|
+
result = client.dominant_stream
|
|
127
|
+
expect(result[:success]).to be false
|
|
128
|
+
expect(result[:error]).to eq(:no_streams)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns stream with highest effective_weight' do
|
|
132
|
+
client.add_stream(stream_type: :emotional, content: {}, weight: 0.2, confidence: 0.2)
|
|
133
|
+
high = client.add_stream(stream_type: :memorial, content: {}, weight: 0.95, confidence: 0.95)
|
|
134
|
+
result = client.dominant_stream
|
|
135
|
+
expect(result[:stream][:id]).to eq(high[:stream_id])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '#synthesis_history' do
|
|
140
|
+
it 'returns empty list initially' do
|
|
141
|
+
result = client.synthesis_history
|
|
142
|
+
expect(result[:syntheses]).to be_empty
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'returns syntheses after multiple runs' do
|
|
146
|
+
add_two_streams
|
|
147
|
+
3.times { client.synthesize }
|
|
148
|
+
result = client.synthesis_history(limit: 2)
|
|
149
|
+
expect(result[:count]).to eq(2)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe '#average_coherence' do
|
|
154
|
+
it 'returns 0.0 when no syntheses' do
|
|
155
|
+
result = client.average_coherence
|
|
156
|
+
expect(result[:average_coherence]).to eq(0.0)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'returns coherence value after syntheses' do
|
|
160
|
+
add_two_streams
|
|
161
|
+
3.times { client.synthesize }
|
|
162
|
+
result = client.average_coherence
|
|
163
|
+
expect(result[:average_coherence]).to be_between(0.0, 1.0)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#status' do
|
|
168
|
+
it 'returns success with stats' do
|
|
169
|
+
result = client.status
|
|
170
|
+
expect(result[:success]).to be true
|
|
171
|
+
expect(result).to have_key(:stream_count)
|
|
172
|
+
expect(result).to have_key(:synthesis_count)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'reflects added streams' do
|
|
176
|
+
add_two_streams
|
|
177
|
+
result = client.status
|
|
178
|
+
expect(result[:stream_count]).to eq(2)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
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/cognitive_synthesis'
|
|
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
|