lex-cognitive-chunking 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 +15 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/lex-cognitive-chunking.gemspec +29 -0
- data/lib/legion/extensions/cognitive_chunking/client.rb +17 -0
- data/lib/legion/extensions/cognitive_chunking/helpers/chunk.rb +88 -0
- data/lib/legion/extensions/cognitive_chunking/helpers/chunking_engine.rb +143 -0
- data/lib/legion/extensions/cognitive_chunking/helpers/constants.rb +52 -0
- data/lib/legion/extensions/cognitive_chunking/helpers/information_item.rb +47 -0
- data/lib/legion/extensions/cognitive_chunking/runners/cognitive_chunking.rb +107 -0
- data/lib/legion/extensions/cognitive_chunking/version.rb +9 -0
- data/lib/legion/extensions/cognitive_chunking.rb +17 -0
- data/spec/legion/extensions/cognitive_chunking/client_spec.rb +67 -0
- data/spec/legion/extensions/cognitive_chunking/helpers/chunk_spec.rb +187 -0
- data/spec/legion/extensions/cognitive_chunking/helpers/chunking_engine_spec.rb +290 -0
- data/spec/legion/extensions/cognitive_chunking/helpers/constants_spec.rb +116 -0
- data/spec/legion/extensions/cognitive_chunking/helpers/information_item_spec.rb +75 -0
- data/spec/legion/extensions/cognitive_chunking/runners/cognitive_chunking_spec.rb +169 -0
- data/spec/spec_helper.rb +20 -0
- metadata +80 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::Chunk do
|
|
4
|
+
let(:chunk) { described_class.new(label: 'chess opening') }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns a uuid id' do
|
|
8
|
+
expect(chunk.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'stores label' do
|
|
12
|
+
expect(chunk.label).to eq('chess opening')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'starts with empty item_ids' do
|
|
16
|
+
expect(chunk.item_ids).to be_empty
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts with empty sub_chunk_ids' do
|
|
20
|
+
expect(chunk.sub_chunk_ids).to be_empty
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'starts with DEFAULT_COHERENCE' do
|
|
24
|
+
expect(chunk.coherence).to eq(Legion::Extensions::CognitiveChunking::Helpers::Constants::DEFAULT_COHERENCE)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'starts with recall_strength of 0.8' do
|
|
28
|
+
expect(chunk.recall_strength).to eq(0.8)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'starts with access_count of 0' do
|
|
32
|
+
expect(chunk.access_count).to eq(0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'pre-populates item_ids from constructor' do
|
|
36
|
+
c = described_class.new(label: 'test', item_ids: %w[a b c])
|
|
37
|
+
expect(c.item_ids).to eq(%w[a b c])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'does not share item_ids reference' do
|
|
41
|
+
ids = %w[a b]
|
|
42
|
+
c = described_class.new(label: 'test', item_ids: ids)
|
|
43
|
+
ids << 'c'
|
|
44
|
+
expect(c.item_ids.size).to eq(2)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#add_item!' do
|
|
49
|
+
it 'adds item_id to item_ids' do
|
|
50
|
+
chunk.add_item!(item_id: 'abc')
|
|
51
|
+
expect(chunk.item_ids).to include('abc')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'does not add duplicates' do
|
|
55
|
+
chunk.add_item!(item_id: 'abc')
|
|
56
|
+
chunk.add_item!(item_id: 'abc')
|
|
57
|
+
expect(chunk.item_ids.count('abc')).to eq(1)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '#remove_item!' do
|
|
62
|
+
before { chunk.add_item!(item_id: 'abc') }
|
|
63
|
+
|
|
64
|
+
it 'removes item_id' do
|
|
65
|
+
chunk.remove_item!(item_id: 'abc')
|
|
66
|
+
expect(chunk.item_ids).not_to include('abc')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#add_sub_chunk!' do
|
|
71
|
+
it 'adds sub_chunk_id' do
|
|
72
|
+
chunk.add_sub_chunk!(chunk_id: 'sub-1')
|
|
73
|
+
expect(chunk.sub_chunk_ids).to include('sub-1')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'does not add duplicates' do
|
|
77
|
+
chunk.add_sub_chunk!(chunk_id: 'sub-1')
|
|
78
|
+
chunk.add_sub_chunk!(chunk_id: 'sub-1')
|
|
79
|
+
expect(chunk.sub_chunk_ids.count('sub-1')).to eq(1)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#reinforce!' do
|
|
84
|
+
it 'increments access_count' do
|
|
85
|
+
chunk.reinforce!
|
|
86
|
+
expect(chunk.access_count).to eq(1)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'boosts coherence' do
|
|
90
|
+
original = chunk.coherence
|
|
91
|
+
chunk.reinforce!
|
|
92
|
+
expect(chunk.coherence).to be > original
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'boosts recall_strength' do
|
|
96
|
+
original = chunk.recall_strength
|
|
97
|
+
chunk.reinforce!
|
|
98
|
+
expect(chunk.recall_strength).to be > original
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'caps coherence at 1.0' do
|
|
102
|
+
20.times { chunk.reinforce! }
|
|
103
|
+
expect(chunk.coherence).to eq(1.0)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'caps recall_strength at 1.0' do
|
|
107
|
+
20.times { chunk.reinforce! }
|
|
108
|
+
expect(chunk.recall_strength).to eq(1.0)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe '#decay!' do
|
|
113
|
+
it 'reduces recall_strength' do
|
|
114
|
+
original = chunk.recall_strength
|
|
115
|
+
chunk.decay!
|
|
116
|
+
expect(chunk.recall_strength).to be < original
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'reduces coherence' do
|
|
120
|
+
original = chunk.coherence
|
|
121
|
+
chunk.decay!
|
|
122
|
+
expect(chunk.coherence).to be < original
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'floors recall_strength at 0.0' do
|
|
126
|
+
100.times { chunk.decay! }
|
|
127
|
+
expect(chunk.recall_strength).to eq(0.0)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '#size' do
|
|
132
|
+
it 'returns count of item_ids' do
|
|
133
|
+
chunk.add_item!(item_id: 'a')
|
|
134
|
+
chunk.add_item!(item_id: 'b')
|
|
135
|
+
expect(chunk.size).to eq(2)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '#hierarchical?' do
|
|
140
|
+
it 'returns false when no sub_chunks' do
|
|
141
|
+
expect(chunk.hierarchical?).to be false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'returns true when sub_chunks exist' do
|
|
145
|
+
chunk.add_sub_chunk!(chunk_id: 'child')
|
|
146
|
+
expect(chunk.hierarchical?).to be true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#coherence_label' do
|
|
151
|
+
it 'returns :loosely_chunked for default coherence (0.5)' do
|
|
152
|
+
expect(chunk.coherence_label).to eq(:loosely_chunked)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe '#recall_label' do
|
|
157
|
+
it 'returns :instant for high recall_strength (0.8)' do
|
|
158
|
+
expect(chunk.recall_label).to eq(:instant)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe '#size_label' do
|
|
163
|
+
it 'returns :micro for empty chunk' do
|
|
164
|
+
expect(chunk.size_label).to eq(:micro)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'returns :large for 7+ items' do
|
|
168
|
+
7.times { |i| chunk.add_item!(item_id: "item-#{i}") }
|
|
169
|
+
expect(chunk.size_label).to eq(:large)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe '#to_h' do
|
|
174
|
+
it 'includes all expected keys' do
|
|
175
|
+
h = chunk.to_h
|
|
176
|
+
expected = %i[id label item_ids sub_chunk_ids coherence recall_strength
|
|
177
|
+
access_count created_at size hierarchical coherence_label
|
|
178
|
+
recall_label size_label]
|
|
179
|
+
expect(h.keys).to include(*expected)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'rounds coherence to 10 decimal places' do
|
|
183
|
+
chunk.reinforce!
|
|
184
|
+
expect(chunk.to_h[:coherence]).to eq(chunk.coherence.round(10))
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::ChunkingEngine do
|
|
4
|
+
let(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
def add_items(count, domain: :general)
|
|
7
|
+
count.times.map { |i| engine.add_item(content: "item #{i}", domain: domain) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#add_item' do
|
|
11
|
+
it 'returns success with item_id' do
|
|
12
|
+
result = engine.add_item(content: 'rook pins queen', domain: :chess)
|
|
13
|
+
expect(result[:success]).to be true
|
|
14
|
+
expect(result[:item_id]).to be_a(String)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'stores the item' do
|
|
18
|
+
result = engine.add_item(content: 'test', domain: :general)
|
|
19
|
+
expect(engine.items[result[:item_id]]).not_to be_nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'returns item hash in result' do
|
|
23
|
+
result = engine.add_item(content: 'test')
|
|
24
|
+
expect(result[:item]).to have_key(:id)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#create_chunk' do
|
|
29
|
+
let(:items) { add_items(3) }
|
|
30
|
+
let(:item_ids) { items.map { |r| r[:item_id] } }
|
|
31
|
+
|
|
32
|
+
it 'returns success with chunk_id' do
|
|
33
|
+
result = engine.create_chunk(label: 'group', item_ids: item_ids)
|
|
34
|
+
expect(result[:success]).to be true
|
|
35
|
+
expect(result[:chunk_id]).to be_a(String)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'stores the chunk' do
|
|
39
|
+
result = engine.create_chunk(label: 'group', item_ids: item_ids)
|
|
40
|
+
expect(engine.chunks[result[:chunk_id]]).not_to be_nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'marks items as chunked' do
|
|
44
|
+
result = engine.create_chunk(label: 'group', item_ids: item_ids)
|
|
45
|
+
item_ids.each do |id|
|
|
46
|
+
expect(engine.items[id].chunked?).to be true
|
|
47
|
+
expect(engine.items[id].chunk_id).to eq(result[:chunk_id])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'fails for empty item_ids' do
|
|
52
|
+
result = engine.create_chunk(label: 'empty', item_ids: [])
|
|
53
|
+
expect(result[:success]).to be false
|
|
54
|
+
expect(result[:error]).to eq(:empty_item_ids)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'fails for non-existent item_ids' do
|
|
58
|
+
result = engine.create_chunk(label: 'bad', item_ids: %w[nonexistent])
|
|
59
|
+
expect(result[:success]).to be false
|
|
60
|
+
expect(result[:error]).to eq(:no_valid_items)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#merge_chunks' do
|
|
65
|
+
def setup_two_chunks
|
|
66
|
+
items_a = add_items(2)
|
|
67
|
+
items_b = add_items(2)
|
|
68
|
+
chunk_a = engine.create_chunk(label: 'A', item_ids: items_a.map { |r| r[:item_id] })
|
|
69
|
+
chunk_b = engine.create_chunk(label: 'B', item_ids: items_b.map { |r| r[:item_id] })
|
|
70
|
+
[chunk_a[:chunk_id], chunk_b[:chunk_id]]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'creates a hierarchical parent chunk' do
|
|
74
|
+
chunk_ids = setup_two_chunks
|
|
75
|
+
result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
|
|
76
|
+
expect(result[:success]).to be true
|
|
77
|
+
parent = engine.chunks[result[:chunk_id]]
|
|
78
|
+
expect(parent.hierarchical?).to be true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'parent has all item_ids from children' do
|
|
82
|
+
chunk_ids = setup_two_chunks
|
|
83
|
+
result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
|
|
84
|
+
parent = engine.chunks[result[:chunk_id]]
|
|
85
|
+
expect(parent.item_ids.size).to eq(4)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'includes merged_from in result' do
|
|
89
|
+
chunk_ids = setup_two_chunks
|
|
90
|
+
result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
|
|
91
|
+
expect(result[:merged_from]).to match_array(chunk_ids)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'fails with fewer than 2 chunk_ids' do
|
|
95
|
+
add_items(2)
|
|
96
|
+
result = engine.merge_chunks(chunk_ids: ['only-one'], label: 'Bad')
|
|
97
|
+
expect(result[:success]).to be false
|
|
98
|
+
expect(result[:error]).to eq(:insufficient_chunks)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#load_to_working_memory' do
|
|
103
|
+
let(:item_id) { engine.add_item(content: 'test')[:item_id] }
|
|
104
|
+
let(:chunk_id) { engine.create_chunk(label: 'wm test', item_ids: [item_id])[:chunk_id] }
|
|
105
|
+
|
|
106
|
+
it 'adds chunk to working memory' do
|
|
107
|
+
result = engine.load_to_working_memory(chunk_id: chunk_id)
|
|
108
|
+
expect(result[:success]).to be true
|
|
109
|
+
expect(engine.working_memory).to include(chunk_id)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'reinforces the chunk on load' do
|
|
113
|
+
original_count = engine.chunks[chunk_id].access_count
|
|
114
|
+
engine.load_to_working_memory(chunk_id: chunk_id)
|
|
115
|
+
expect(engine.chunks[chunk_id].access_count).to be > original_count
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'fails for unknown chunk' do
|
|
119
|
+
result = engine.load_to_working_memory(chunk_id: 'nonexistent')
|
|
120
|
+
expect(result[:success]).to be false
|
|
121
|
+
expect(result[:error]).to eq(:chunk_not_found)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'fails when already loaded' do
|
|
125
|
+
engine.load_to_working_memory(chunk_id: chunk_id)
|
|
126
|
+
result = engine.load_to_working_memory(chunk_id: chunk_id)
|
|
127
|
+
expect(result[:success]).to be false
|
|
128
|
+
expect(result[:error]).to eq(:already_loaded)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'fails when working memory is at capacity' do
|
|
132
|
+
# Fill working memory to WORKING_MEMORY_CAPACITY
|
|
133
|
+
7.times do
|
|
134
|
+
id = engine.add_item(content: 'filler')[:item_id]
|
|
135
|
+
cid = engine.create_chunk(label: 'filler', item_ids: [id])[:chunk_id]
|
|
136
|
+
engine.load_to_working_memory(chunk_id: cid)
|
|
137
|
+
end
|
|
138
|
+
result = engine.load_to_working_memory(chunk_id: chunk_id)
|
|
139
|
+
expect(result[:success]).to be false
|
|
140
|
+
expect(result[:error]).to eq(:capacity_exceeded)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '#unload_from_working_memory' do
|
|
145
|
+
let(:item_id) { engine.add_item(content: 'test')[:item_id] }
|
|
146
|
+
let(:chunk_id) { engine.create_chunk(label: 'unload test', item_ids: [item_id])[:chunk_id] }
|
|
147
|
+
|
|
148
|
+
before { engine.load_to_working_memory(chunk_id: chunk_id) }
|
|
149
|
+
|
|
150
|
+
it 'removes chunk from working memory' do
|
|
151
|
+
engine.unload_from_working_memory(chunk_id: chunk_id)
|
|
152
|
+
expect(engine.working_memory).not_to include(chunk_id)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'returns success' do
|
|
156
|
+
result = engine.unload_from_working_memory(chunk_id: chunk_id)
|
|
157
|
+
expect(result[:success]).to be true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'fails if not in working memory' do
|
|
161
|
+
engine.unload_from_working_memory(chunk_id: chunk_id)
|
|
162
|
+
result = engine.unload_from_working_memory(chunk_id: chunk_id)
|
|
163
|
+
expect(result[:success]).to be false
|
|
164
|
+
expect(result[:error]).to eq(:not_in_working_memory)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#working_memory_load' do
|
|
169
|
+
it 'returns 0.0 for empty working memory' do
|
|
170
|
+
expect(engine.working_memory_load).to eq(0.0)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'returns load as a ratio' do
|
|
174
|
+
id = engine.add_item(content: 'x')[:item_id]
|
|
175
|
+
cid = engine.create_chunk(label: 'x', item_ids: [id])[:chunk_id]
|
|
176
|
+
engine.load_to_working_memory(chunk_id: cid)
|
|
177
|
+
expected = (1.0 / 7).round(10)
|
|
178
|
+
expect(engine.working_memory_load).to be_within(0.0001).of(expected)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe '#working_memory_overloaded?' do
|
|
183
|
+
it 'returns false when under capacity' do
|
|
184
|
+
expect(engine.working_memory_overloaded?).to be false
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe '#decay_all!' do
|
|
189
|
+
it 'decays all chunks' do
|
|
190
|
+
id = engine.add_item(content: 'x')[:item_id]
|
|
191
|
+
cid = engine.create_chunk(label: 'x', item_ids: [id])[:chunk_id]
|
|
192
|
+
original_recall = engine.chunks[cid].recall_strength
|
|
193
|
+
engine.decay_all!
|
|
194
|
+
expect(engine.chunks[cid].recall_strength).to be < original_recall
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'returns success with count' do
|
|
198
|
+
add_items(3).each_with_index do |r, i|
|
|
199
|
+
engine.create_chunk(label: "c#{i}", item_ids: [r[:item_id]])
|
|
200
|
+
end
|
|
201
|
+
result = engine.decay_all!
|
|
202
|
+
expect(result[:success]).to be true
|
|
203
|
+
expect(result[:chunks_decayed]).to eq(3)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
describe '#reinforce_chunk' do
|
|
208
|
+
let(:item_id) { engine.add_item(content: 'reinforce me')[:item_id] }
|
|
209
|
+
let(:chunk_id) { engine.create_chunk(label: 'reinforce', item_ids: [item_id])[:chunk_id] }
|
|
210
|
+
|
|
211
|
+
it 'boosts recall strength' do
|
|
212
|
+
before_recall = engine.chunks[chunk_id].recall_strength
|
|
213
|
+
engine.reinforce_chunk(chunk_id: chunk_id)
|
|
214
|
+
expect(engine.chunks[chunk_id].recall_strength).to be > before_recall
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'fails for unknown chunk_id' do
|
|
218
|
+
result = engine.reinforce_chunk(chunk_id: 'bad')
|
|
219
|
+
expect(result[:success]).to be false
|
|
220
|
+
expect(result[:error]).to eq(:chunk_not_found)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
describe '#strongest_chunks' do
|
|
225
|
+
it 'returns chunks sorted by recall_strength descending' do
|
|
226
|
+
r1 = engine.add_item(content: 'a')[:item_id]
|
|
227
|
+
r2 = engine.add_item(content: 'b')[:item_id]
|
|
228
|
+
c1 = engine.create_chunk(label: 'weak', item_ids: [r1])[:chunk_id]
|
|
229
|
+
c2 = engine.create_chunk(label: 'strong', item_ids: [r2])[:chunk_id]
|
|
230
|
+
engine.chunks[c1].decay!
|
|
231
|
+
engine.chunks[c2].reinforce!
|
|
232
|
+
chunks = engine.strongest_chunks(limit: 2)
|
|
233
|
+
expect(chunks.first[:recall_strength]).to be >= chunks.last[:recall_strength]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'respects limit' do
|
|
237
|
+
add_items(5).each_with_index { |r, i| engine.create_chunk(label: "c#{i}", item_ids: [r[:item_id]]) }
|
|
238
|
+
expect(engine.strongest_chunks(limit: 3).size).to eq(3)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
describe '#unchunked_items' do
|
|
243
|
+
it 'returns items not in any chunk' do
|
|
244
|
+
r1 = engine.add_item(content: 'chunked item')[:item_id]
|
|
245
|
+
engine.add_item(content: 'free item')
|
|
246
|
+
engine.create_chunk(label: 'group', item_ids: [r1])
|
|
247
|
+
unchunked = engine.unchunked_items
|
|
248
|
+
expect(unchunked.size).to eq(1)
|
|
249
|
+
expect(unchunked.first[:content]).to eq('free item')
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
describe '#chunking_efficiency' do
|
|
254
|
+
it 'returns 0.0 for no items' do
|
|
255
|
+
expect(engine.chunking_efficiency).to eq(0.0)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'returns ratio of chunked to total' do
|
|
259
|
+
r1 = engine.add_item(content: 'a')[:item_id]
|
|
260
|
+
engine.add_item(content: 'b')
|
|
261
|
+
engine.create_chunk(label: 'test', item_ids: [r1])
|
|
262
|
+
expect(engine.chunking_efficiency).to be_within(0.001).of(0.5)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
describe '#chunking_report' do
|
|
267
|
+
it 'includes required report keys' do
|
|
268
|
+
report = engine.chunking_report
|
|
269
|
+
expect(report).to have_key(:total_items)
|
|
270
|
+
expect(report).to have_key(:total_chunks)
|
|
271
|
+
expect(report).to have_key(:unchunked_items)
|
|
272
|
+
expect(report).to have_key(:chunking_efficiency)
|
|
273
|
+
expect(report).to have_key(:working_memory)
|
|
274
|
+
expect(report).to have_key(:strongest_chunks)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'includes working memory capacity label' do
|
|
278
|
+
expect(engine.chunking_report[:working_memory][:label]).to be_a(Symbol)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
describe '#to_h' do
|
|
283
|
+
it 'includes items, chunks, and working_memory keys' do
|
|
284
|
+
h = engine.to_h
|
|
285
|
+
expect(h).to have_key(:items)
|
|
286
|
+
expect(h).to have_key(:chunks)
|
|
287
|
+
expect(h).to have_key(:working_memory)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::Constants do
|
|
4
|
+
describe 'core capacity constants' do
|
|
5
|
+
it 'defines MAX_ITEMS as 500' do
|
|
6
|
+
expect(described_class::MAX_ITEMS).to eq(500)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'defines MAX_CHUNKS as 200' do
|
|
10
|
+
expect(described_class::MAX_CHUNKS).to eq(200)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'defines WORKING_MEMORY_CAPACITY as 7' do
|
|
14
|
+
expect(described_class::WORKING_MEMORY_CAPACITY).to eq(7)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'defines CAPACITY_VARIANCE as 2' do
|
|
18
|
+
expect(described_class::CAPACITY_VARIANCE).to eq(2)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe 'score constants' do
|
|
23
|
+
it 'defines DEFAULT_COHERENCE as 0.5' do
|
|
24
|
+
expect(described_class::DEFAULT_COHERENCE).to eq(0.5)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'defines COHERENCE_BOOST as 0.08' do
|
|
28
|
+
expect(described_class::COHERENCE_BOOST).to eq(0.08)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'defines COHERENCE_DECAY as 0.03' do
|
|
32
|
+
expect(described_class::COHERENCE_DECAY).to eq(0.03)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'defines RECALL_DECAY as 0.02' do
|
|
36
|
+
expect(described_class::RECALL_DECAY).to eq(0.02)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'defines RECALL_BOOST as 0.1' do
|
|
40
|
+
expect(described_class::RECALL_BOOST).to eq(0.1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe 'CHUNK_SIZE_LABELS' do
|
|
45
|
+
it 'labels size 7 as large' do
|
|
46
|
+
label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(7) }&.last
|
|
47
|
+
expect(label).to eq(:large)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'labels size 5 as medium' do
|
|
51
|
+
label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(5) }&.last
|
|
52
|
+
expect(label).to eq(:medium)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'labels size 3 as small' do
|
|
56
|
+
label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(3) }&.last
|
|
57
|
+
expect(label).to eq(:small)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'labels size 1 as micro' do
|
|
61
|
+
label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(1) }&.last
|
|
62
|
+
expect(label).to eq(:micro)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'COHERENCE_LABELS' do
|
|
67
|
+
it 'labels 0.9 as tightly_chunked' do
|
|
68
|
+
label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.9) }&.last
|
|
69
|
+
expect(label).to eq(:tightly_chunked)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'labels 0.5 as loosely_chunked' do
|
|
73
|
+
label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.5) }&.last
|
|
74
|
+
expect(label).to eq(:loosely_chunked)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'labels 0.1 as unchunked' do
|
|
78
|
+
label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.1) }&.last
|
|
79
|
+
expect(label).to eq(:unchunked)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe 'RECALL_LABELS' do
|
|
84
|
+
it 'labels 0.9 as instant' do
|
|
85
|
+
label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.9) }&.last
|
|
86
|
+
expect(label).to eq(:instant)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'labels 0.5 as moderate' do
|
|
90
|
+
label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.5) }&.last
|
|
91
|
+
expect(label).to eq(:moderate)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'labels 0.1 as forgotten' do
|
|
95
|
+
label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.1) }&.last
|
|
96
|
+
expect(label).to eq(:forgotten)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe 'CAPACITY_LABELS' do
|
|
101
|
+
it 'labels 0.9 as overloaded' do
|
|
102
|
+
label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.9) }&.last
|
|
103
|
+
expect(label).to eq(:overloaded)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'labels 0.5 as comfortable' do
|
|
107
|
+
label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.5) }&.last
|
|
108
|
+
expect(label).to eq(:comfortable)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'labels 0.1 as empty' do
|
|
112
|
+
label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.1) }&.last
|
|
113
|
+
expect(label).to eq(:empty)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::InformationItem do
|
|
4
|
+
let(:item) { described_class.new(content: 'bishop controls e4', domain: :chess) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns a uuid id' do
|
|
8
|
+
expect(item.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'stores content' do
|
|
12
|
+
expect(item.content).to eq('bishop controls e4')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'stores domain' do
|
|
16
|
+
expect(item.domain).to eq(:chess)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'defaults chunked to false' do
|
|
20
|
+
expect(item.chunked?).to be false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'defaults chunk_id to nil' do
|
|
24
|
+
expect(item.chunk_id).to be_nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'sets created_at' do
|
|
28
|
+
expect(item.created_at).to be_a(Time)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'defaults domain to :general' do
|
|
32
|
+
plain = described_class.new(content: 'hello')
|
|
33
|
+
expect(plain.domain).to eq(:general)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#assign_to_chunk!' do
|
|
38
|
+
it 'marks item as chunked' do
|
|
39
|
+
item.assign_to_chunk!(chunk_id: 'abc-123')
|
|
40
|
+
expect(item.chunked?).to be true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'stores the chunk_id' do
|
|
44
|
+
item.assign_to_chunk!(chunk_id: 'abc-123')
|
|
45
|
+
expect(item.chunk_id).to eq('abc-123')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#unchunk!' do
|
|
50
|
+
before { item.assign_to_chunk!(chunk_id: 'abc-123') }
|
|
51
|
+
|
|
52
|
+
it 'clears chunked flag' do
|
|
53
|
+
item.unchunk!
|
|
54
|
+
expect(item.chunked?).to be false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'clears chunk_id' do
|
|
58
|
+
item.unchunk!
|
|
59
|
+
expect(item.chunk_id).to be_nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#to_h' do
|
|
64
|
+
it 'includes all expected keys' do
|
|
65
|
+
h = item.to_h
|
|
66
|
+
expect(h.keys).to contain_exactly(:id, :content, :domain, :chunked, :chunk_id, :created_at)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'reflects chunked state' do
|
|
70
|
+
item.assign_to_chunk!(chunk_id: 'xyz')
|
|
71
|
+
expect(item.to_h[:chunked]).to be true
|
|
72
|
+
expect(item.to_h[:chunk_id]).to eq('xyz')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|