lex-cognitive-catalyst 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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCatalyst
6
+ module Runners
7
+ module CognitiveCatalyst
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_catalyst(catalyst_type:, domain:, potency: nil, specificity: nil, engine: nil, **)
12
+ e = engine || default_engine
13
+ unless Helpers::Constants::CATALYST_TYPES.include?(catalyst_type.to_sym)
14
+ raise ArgumentError, "invalid catalyst_type: #{catalyst_type}"
15
+ end
16
+
17
+ opts = {
18
+ catalyst_type: catalyst_type.to_sym,
19
+ domain: domain
20
+ }
21
+ opts[:potency] = potency unless potency.nil?
22
+ opts[:specificity] = specificity unless specificity.nil?
23
+
24
+ catalyst = e.create_catalyst(**opts)
25
+ Legion::Logging.debug "[cognitive_catalyst] create_catalyst id=#{catalyst.id[0..7]} " \
26
+ "type=#{catalyst_type} domain=#{domain} potency=#{catalyst.potency.round(2)}"
27
+ { success: true, catalyst: catalyst.to_h }
28
+ rescue ArgumentError => e
29
+ Legion::Logging.warn "[cognitive_catalyst] create_catalyst failed: #{e.message}"
30
+ { success: false, reason: e.message }
31
+ end
32
+
33
+ def create_reaction(reaction_type:, reactants:, activation_energy: nil, engine: nil, **)
34
+ e = engine || default_engine
35
+ unless Helpers::Constants::REACTION_TYPES.include?(reaction_type.to_sym)
36
+ raise ArgumentError, "invalid reaction_type: #{reaction_type}"
37
+ end
38
+
39
+ opts = {
40
+ reaction_type: reaction_type.to_sym,
41
+ reactants: Array(reactants)
42
+ }
43
+ opts[:activation_energy] = activation_energy unless activation_energy.nil?
44
+
45
+ reaction = e.create_reaction(**opts)
46
+ Legion::Logging.debug "[cognitive_catalyst] create_reaction id=#{reaction.id[0..7]} " \
47
+ "type=#{reaction_type} reactants=#{reaction.reactants.size}"
48
+ { success: true, reaction: reaction.to_h }
49
+ rescue ArgumentError => e
50
+ Legion::Logging.warn "[cognitive_catalyst] create_reaction failed: #{e.message}"
51
+ { success: false, reason: e.message }
52
+ end
53
+
54
+ def apply_catalyst(catalyst_id:, reaction_id:, engine: nil, **)
55
+ e = engine || default_engine
56
+ result = e.apply_catalyst(catalyst_id: catalyst_id, reaction_id: reaction_id)
57
+ Legion::Logging.debug '[cognitive_catalyst] apply_catalyst ' \
58
+ "catalyst=#{catalyst_id[0..7]} reaction=#{reaction_id[0..7]} " \
59
+ "success=#{result[:success]}"
60
+ result
61
+ end
62
+
63
+ def attempt_reaction(reaction_id:, energy_input:, engine: nil, **)
64
+ e = engine || default_engine
65
+ result = e.attempt_reaction(reaction_id: reaction_id, energy_input: energy_input)
66
+ Legion::Logging.debug "[cognitive_catalyst] attempt_reaction id=#{reaction_id[0..7]} " \
67
+ "energy=#{energy_input} completed=#{result[:completed]}"
68
+ result
69
+ end
70
+
71
+ def recharge(catalyst_id:, amount:, engine: nil, **)
72
+ e = engine || default_engine
73
+ result = e.recharge_catalyst(catalyst_id: catalyst_id, amount: amount)
74
+ Legion::Logging.debug "[cognitive_catalyst] recharge id=#{catalyst_id[0..7]} " \
75
+ "amount=#{amount} potency=#{result[:potency]&.round(2)}"
76
+ result
77
+ end
78
+
79
+ def list_catalysts(engine: nil, **)
80
+ e = engine || default_engine
81
+ catalysts = e.all_catalysts
82
+ Legion::Logging.debug "[cognitive_catalyst] list_catalysts count=#{catalysts.size}"
83
+ { success: true, catalysts: catalysts.map(&:to_h), count: catalysts.size }
84
+ end
85
+
86
+ def catalyst_status(engine: nil, **)
87
+ e = engine || default_engine
88
+ report = e.catalyst_report
89
+ Legion::Logging.debug "[cognitive_catalyst] catalyst_status total=#{report[:total_catalysts]} " \
90
+ "reactions=#{report[:total_reactions]} rate=#{report[:catalyzed_rate].round(2)}"
91
+ { success: true }.merge(report)
92
+ end
93
+
94
+ private
95
+
96
+ def default_engine
97
+ @default_engine ||= Helpers::CatalystEngine.new
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCatalyst
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require_relative 'cognitive_catalyst/version'
6
+ require_relative 'cognitive_catalyst/helpers/constants'
7
+ require_relative 'cognitive_catalyst/helpers/catalyst'
8
+ require_relative 'cognitive_catalyst/helpers/reaction'
9
+ require_relative 'cognitive_catalyst/helpers/catalyst_engine'
10
+ require_relative 'cognitive_catalyst/runners/cognitive_catalyst'
11
+ require_relative 'cognitive_catalyst/client'
12
+
13
+ module Legion
14
+ module Extensions
15
+ module CognitiveCatalyst
16
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveCatalyst::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'includes Runners::CognitiveCatalyst' do
7
+ expect(client).to respond_to(:create_catalyst)
8
+ expect(client).to respond_to(:create_reaction)
9
+ expect(client).to respond_to(:apply_catalyst)
10
+ expect(client).to respond_to(:attempt_reaction)
11
+ expect(client).to respond_to(:recharge)
12
+ expect(client).to respond_to(:list_catalysts)
13
+ expect(client).to respond_to(:catalyst_status)
14
+ end
15
+
16
+ it 'can create and then apply a catalyst to a reaction' do
17
+ engine = Legion::Extensions::CognitiveCatalyst::Helpers::CatalystEngine.new
18
+ cat = client.create_catalyst(catalyst_type: :experience, domain: :learning,
19
+ potency: 0.9, specificity: 0.9, engine: engine)
20
+ rxn = client.create_reaction(reaction_type: :synthesis, reactants: %w[a b], engine: engine)
21
+ result = client.apply_catalyst(catalyst_id: cat[:catalyst][:id],
22
+ reaction_id: rxn[:reaction][:id],
23
+ engine: engine)
24
+ expect(result[:success]).to be true
25
+ expect(result[:activation_energy]).to be < 0.6
26
+ end
27
+
28
+ it 'can complete a full catalyst workflow: create -> apply -> attempt -> report' do
29
+ engine = Legion::Extensions::CognitiveCatalyst::Helpers::CatalystEngine.new
30
+ cat = client.create_catalyst(catalyst_type: :insight, domain: :reasoning,
31
+ potency: 1.0, specificity: 1.0, engine: engine)
32
+ rxn = client.create_reaction(reaction_type: :decomposition,
33
+ reactants: %w[complex_idea],
34
+ activation_energy: 0.1,
35
+ engine: engine)
36
+ client.apply_catalyst(catalyst_id: cat[:catalyst][:id],
37
+ reaction_id: rxn[:reaction][:id],
38
+ engine: engine)
39
+ result = client.attempt_reaction(reaction_id: rxn[:reaction][:id],
40
+ energy_input: 0.8,
41
+ engine: engine)
42
+ expect(result[:completed]).to be true
43
+ expect(result[:catalyzed]).to be true
44
+ status = client.catalyst_status(engine: engine)
45
+ expect(status[:catalyzed_count]).to eq(1)
46
+ expect(status[:catalyzed_rate]).to eq(1.0)
47
+ end
48
+
49
+ it 'maintains separate state per client instance' do
50
+ c1 = described_class.new
51
+ c2 = described_class.new
52
+ c1.create_catalyst(catalyst_type: :insight, domain: :d)
53
+ s1 = c1.catalyst_status
54
+ s2 = c2.catalyst_status
55
+ expect(s1[:total_catalysts]).to eq(1)
56
+ expect(s2[:total_catalysts]).to eq(0)
57
+ end
58
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveCatalyst::Helpers::CatalystEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ let(:constants) { Legion::Extensions::CognitiveCatalyst::Helpers::Constants }
7
+
8
+ def build_catalyst(potency: 0.7, specificity: 0.6)
9
+ engine.create_catalyst(catalyst_type: :insight, domain: :reasoning,
10
+ potency: potency, specificity: specificity)
11
+ end
12
+
13
+ def build_reaction(activation_energy: nil)
14
+ opts = { reaction_type: :synthesis, reactants: %w[a b] }
15
+ opts[:activation_energy] = activation_energy if activation_energy
16
+ engine.create_reaction(**opts)
17
+ end
18
+
19
+ describe '#create_catalyst' do
20
+ it 'creates and returns a Catalyst' do
21
+ result = build_catalyst
22
+ expect(result).to be_a(Legion::Extensions::CognitiveCatalyst::Helpers::Catalyst)
23
+ end
24
+
25
+ it 'stores the catalyst' do
26
+ build_catalyst
27
+ expect(engine.all_catalysts.size).to eq(1)
28
+ end
29
+
30
+ it 'accepts all valid catalyst types' do
31
+ constants::CATALYST_TYPES.each do |type|
32
+ expect { engine.create_catalyst(catalyst_type: type, domain: :d) }.not_to raise_error
33
+ end
34
+ end
35
+
36
+ it 'raises ArgumentError for invalid catalyst_type' do
37
+ expect { engine.create_catalyst(catalyst_type: :invalid, domain: :d) }.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it 'evicts oldest when at MAX_CATALYSTS capacity' do
41
+ max = constants::MAX_CATALYSTS
42
+ max.times { |i| engine.create_catalyst(catalyst_type: :insight, domain: "d#{i}") }
43
+ engine.create_catalyst(catalyst_type: :analogy, domain: :overflow)
44
+ expect(engine.all_catalysts.size).to eq(max)
45
+ end
46
+ end
47
+
48
+ describe '#create_reaction' do
49
+ it 'creates and returns a Reaction' do
50
+ result = build_reaction
51
+ expect(result).to be_a(Legion::Extensions::CognitiveCatalyst::Helpers::Reaction)
52
+ end
53
+
54
+ it 'stores the reaction' do
55
+ build_reaction
56
+ expect(engine.all_reactions.size).to eq(1)
57
+ end
58
+
59
+ it 'accepts all valid reaction types' do
60
+ constants::REACTION_TYPES.each do |type|
61
+ expect { engine.create_reaction(reaction_type: type, reactants: ['x']) }.not_to raise_error
62
+ end
63
+ end
64
+
65
+ it 'raises ArgumentError for invalid reaction_type' do
66
+ expect { engine.create_reaction(reaction_type: :explode, reactants: []) }.to raise_error(ArgumentError)
67
+ end
68
+
69
+ it 'evicts oldest when at MAX_REACTIONS capacity' do
70
+ max = constants::MAX_REACTIONS
71
+ max.times { |i| engine.create_reaction(reaction_type: :synthesis, reactants: ["r#{i}"]) }
72
+ engine.create_reaction(reaction_type: :exchange, reactants: [:overflow])
73
+ expect(engine.all_reactions.size).to eq(max)
74
+ end
75
+ end
76
+
77
+ describe '#apply_catalyst' do
78
+ let(:catalyst) { build_catalyst }
79
+ let(:reaction) { build_reaction }
80
+
81
+ it 'returns success with updated activation_energy' do
82
+ result = engine.apply_catalyst(catalyst_id: catalyst.id, reaction_id: reaction.id)
83
+ expect(result[:success]).to be true
84
+ expect(result[:activation_energy]).to be < constants::ACTIVATION_ENERGY
85
+ end
86
+
87
+ it 'returns catalyst_not_found for unknown catalyst' do
88
+ result = engine.apply_catalyst(catalyst_id: 'bad', reaction_id: reaction.id)
89
+ expect(result[:reason]).to eq(:catalyst_not_found)
90
+ end
91
+
92
+ it 'returns reaction_not_found for unknown reaction' do
93
+ result = engine.apply_catalyst(catalyst_id: catalyst.id, reaction_id: 'bad')
94
+ expect(result[:reason]).to eq(:reaction_not_found)
95
+ end
96
+
97
+ it 'returns already_completed for a done reaction' do
98
+ engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
99
+ result = engine.apply_catalyst(catalyst_id: catalyst.id, reaction_id: reaction.id)
100
+ expect(result[:reason]).to eq(:already_completed)
101
+ end
102
+ end
103
+
104
+ describe '#attempt_reaction' do
105
+ let(:reaction) { build_reaction }
106
+
107
+ it 'completes the reaction when energy is sufficient' do
108
+ result = engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
109
+ expect(result[:completed]).to be true
110
+ end
111
+
112
+ it 'does not complete when energy is insufficient' do
113
+ result = engine.attempt_reaction(reaction_id: reaction.id, energy_input: 0.1)
114
+ expect(result[:completed]).to be false
115
+ end
116
+
117
+ it 'returns not_found for unknown reaction' do
118
+ result = engine.attempt_reaction(reaction_id: 'missing', energy_input: 1.0)
119
+ expect(result[:reason]).to eq(:not_found)
120
+ end
121
+
122
+ it 'returns already_completed for a done reaction' do
123
+ engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
124
+ result = engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
125
+ expect(result[:reason]).to eq(:already_completed)
126
+ end
127
+
128
+ it 'includes yield_value and yield_label' do
129
+ result = engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
130
+ expect(result).to include(:yield_value, :yield_label)
131
+ end
132
+ end
133
+
134
+ describe '#degrade_all!' do
135
+ it 'reduces potency of all catalysts' do
136
+ c = build_catalyst(potency: 0.8)
137
+ engine.degrade_all!
138
+ expect(c.potency).to be < 0.8
139
+ end
140
+
141
+ it 'does nothing when no catalysts exist' do
142
+ expect { engine.degrade_all! }.not_to raise_error
143
+ end
144
+ end
145
+
146
+ describe '#recharge_catalyst' do
147
+ let(:catalyst) { build_catalyst(potency: 0.3) }
148
+
149
+ it 'increases potency' do
150
+ result = engine.recharge_catalyst(catalyst_id: catalyst.id, amount: 0.2)
151
+ expect(result[:success]).to be true
152
+ expect(result[:potency]).to be_within(0.001).of(0.5)
153
+ end
154
+
155
+ it 'returns not_found for unknown catalyst' do
156
+ result = engine.recharge_catalyst(catalyst_id: 'bad', amount: 0.1)
157
+ expect(result[:reason]).to eq(:not_found)
158
+ end
159
+
160
+ it 'includes potency_label in result' do
161
+ result = engine.recharge_catalyst(catalyst_id: catalyst.id, amount: 0.6)
162
+ expect(result).to include(:potency_label)
163
+ end
164
+ end
165
+
166
+ describe '#all_catalysts' do
167
+ it 'returns empty array initially' do
168
+ expect(engine.all_catalysts).to eq([])
169
+ end
170
+
171
+ it 'returns all stored catalysts' do
172
+ 2.times { build_catalyst }
173
+ expect(engine.all_catalysts.size).to eq(2)
174
+ end
175
+ end
176
+
177
+ describe '#all_reactions' do
178
+ it 'returns empty array initially' do
179
+ expect(engine.all_reactions).to eq([])
180
+ end
181
+
182
+ it 'returns all stored reactions' do
183
+ 2.times { build_reaction }
184
+ expect(engine.all_reactions.size).to eq(2)
185
+ end
186
+ end
187
+
188
+ describe '#completed_reactions' do
189
+ it 'returns only completed reactions' do
190
+ r1 = build_reaction
191
+ r2 = build_reaction
192
+ engine.attempt_reaction(reaction_id: r1.id, energy_input: 1.0)
193
+ engine.attempt_reaction(reaction_id: r2.id, energy_input: 0.1)
194
+ expect(engine.completed_reactions.size).to eq(1)
195
+ end
196
+ end
197
+
198
+ describe '#catalyzed_rate' do
199
+ it 'returns 0.0 when no reactions completed' do
200
+ expect(engine.catalyzed_rate).to eq(0.0)
201
+ end
202
+
203
+ it 'returns 1.0 when all completed reactions were catalyzed' do
204
+ catalyst = build_catalyst(potency: 1.0, specificity: 1.0)
205
+ reaction = build_reaction(activation_energy: 0.01)
206
+ engine.apply_catalyst(catalyst_id: catalyst.id, reaction_id: reaction.id)
207
+ engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
208
+ expect(engine.catalyzed_rate).to eq(1.0)
209
+ end
210
+
211
+ it 'returns 0.0 when no completed reactions were catalyzed' do
212
+ reaction = build_reaction
213
+ engine.attempt_reaction(reaction_id: reaction.id, energy_input: 1.0)
214
+ expect(engine.catalyzed_rate).to eq(0.0)
215
+ end
216
+
217
+ it 'returns fractional rate for mixed completions' do
218
+ r1 = build_reaction
219
+ r2 = build_reaction
220
+ engine.attempt_reaction(reaction_id: r1.id, energy_input: 1.0)
221
+ catalyst = build_catalyst(potency: 1.0, specificity: 1.0)
222
+ engine.apply_catalyst(catalyst_id: catalyst.id, reaction_id: r2.id)
223
+ engine.attempt_reaction(reaction_id: r2.id, energy_input: 1.0)
224
+ expect(engine.catalyzed_rate).to be_within(0.01).of(0.5)
225
+ end
226
+ end
227
+
228
+ describe '#catalyst_report' do
229
+ it 'returns a report hash with all required keys' do
230
+ report = engine.catalyst_report
231
+ expect(report).to include(
232
+ :total_catalysts, :total_reactions, :completed, :catalyzed_count,
233
+ :catalyzed_rate, :avg_potency, :powerful_count, :inert_count
234
+ )
235
+ end
236
+
237
+ it 'counts powerful catalysts' do
238
+ build_catalyst(potency: 0.9)
239
+ build_catalyst(potency: 0.5)
240
+ report = engine.catalyst_report
241
+ expect(report[:powerful_count]).to eq(1)
242
+ end
243
+
244
+ it 'counts inert catalysts' do
245
+ engine.create_catalyst(catalyst_type: :insight, domain: :d, potency: 0.1)
246
+ engine.create_catalyst(catalyst_type: :insight, domain: :d, potency: 0.5)
247
+ report = engine.catalyst_report
248
+ expect(report[:inert_count]).to eq(1)
249
+ end
250
+
251
+ it 'computes avg_potency' do
252
+ engine.create_catalyst(catalyst_type: :insight, domain: :d, potency: 0.4)
253
+ engine.create_catalyst(catalyst_type: :insight, domain: :d, potency: 0.6)
254
+ report = engine.catalyst_report
255
+ expect(report[:avg_potency]).to be_within(0.01).of(0.5)
256
+ end
257
+
258
+ it 'returns avg_potency of 0.0 when no catalysts' do
259
+ report = engine.catalyst_report
260
+ expect(report[:avg_potency]).to eq(0.0)
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveCatalyst::Helpers::Catalyst do
4
+ subject(:catalyst) { described_class.new(catalyst_type: :insight, domain: :reasoning) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(catalyst.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'assigns catalyst_type' do
12
+ expect(catalyst.catalyst_type).to eq(:insight)
13
+ end
14
+
15
+ it 'assigns domain' do
16
+ expect(catalyst.domain).to eq(:reasoning)
17
+ end
18
+
19
+ it 'defaults potency to 0.5' do
20
+ expect(catalyst.potency).to eq(0.5)
21
+ end
22
+
23
+ it 'defaults specificity to 0.5' do
24
+ expect(catalyst.specificity).to eq(0.5)
25
+ end
26
+
27
+ it 'defaults uses_count to 0' do
28
+ expect(catalyst.uses_count).to eq(0)
29
+ end
30
+
31
+ it 'records created_at' do
32
+ expect(catalyst.created_at).to be_a(Time)
33
+ end
34
+
35
+ it 'clamps potency above 1.0' do
36
+ c = described_class.new(catalyst_type: :analogy, domain: :d, potency: 2.0)
37
+ expect(c.potency).to eq(1.0)
38
+ end
39
+
40
+ it 'clamps potency below 0.0' do
41
+ c = described_class.new(catalyst_type: :analogy, domain: :d, potency: -0.5)
42
+ expect(c.potency).to eq(0.0)
43
+ end
44
+
45
+ it 'clamps specificity above 1.0' do
46
+ c = described_class.new(catalyst_type: :analogy, domain: :d, specificity: 5.0)
47
+ expect(c.specificity).to eq(1.0)
48
+ end
49
+
50
+ it 'accepts custom potency' do
51
+ c = described_class.new(catalyst_type: :pattern, domain: :d, potency: 0.9)
52
+ expect(c.potency).to eq(0.9)
53
+ end
54
+ end
55
+
56
+ describe '#catalyze!' do
57
+ it 'increments uses_count each call' do
58
+ catalyst.catalyze!(:synthesis)
59
+ catalyst.catalyze!(:synthesis)
60
+ expect(catalyst.uses_count).to eq(2)
61
+ end
62
+
63
+ it 'returns activation_reduction = potency * specificity' do
64
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.8, specificity: 0.5)
65
+ reduction = c.catalyze!(:synthesis)
66
+ expect(reduction).to be_within(0.0001).of(0.4)
67
+ end
68
+
69
+ it 'does NOT reduce potency (catalysts are not consumed)' do
70
+ original = catalyst.potency
71
+ catalyst.catalyze!(:synthesis)
72
+ expect(catalyst.potency).to eq(original)
73
+ end
74
+
75
+ it 'returns a float' do
76
+ expect(catalyst.catalyze!(:exchange)).to be_a(Float)
77
+ end
78
+
79
+ it 'works with all reaction types' do
80
+ Legion::Extensions::CognitiveCatalyst::Helpers::Constants::REACTION_TYPES.each do |rt|
81
+ expect { catalyst.catalyze!(rt) }.not_to raise_error
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#degrade!' do
87
+ it 'reduces potency by POTENCY_DECAY' do
88
+ decay = Legion::Extensions::CognitiveCatalyst::Helpers::Constants::POTENCY_DECAY
89
+ original = catalyst.potency
90
+ catalyst.degrade!
91
+ expect(catalyst.potency).to be_within(0.0001).of(original - decay)
92
+ end
93
+
94
+ it 'does not reduce potency below 0.0' do
95
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.01)
96
+ c.degrade!
97
+ expect(c.potency).to eq(0.0)
98
+ end
99
+
100
+ it 'can be called multiple times' do
101
+ 5.times { catalyst.degrade! }
102
+ expect(catalyst.potency).to be >= 0.0
103
+ expect(catalyst.potency).to be < 0.5
104
+ end
105
+ end
106
+
107
+ describe '#recharge!' do
108
+ it 'increases potency by amount' do
109
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.3)
110
+ c.recharge!(0.2)
111
+ expect(c.potency).to be_within(0.0001).of(0.5)
112
+ end
113
+
114
+ it 'clamps at 1.0' do
115
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.9)
116
+ c.recharge!(0.5)
117
+ expect(c.potency).to eq(1.0)
118
+ end
119
+
120
+ it 'returns the new potency' do
121
+ result = catalyst.recharge!(0.1)
122
+ expect(result).to be_a(Float)
123
+ end
124
+ end
125
+
126
+ describe '#powerful?' do
127
+ it 'returns true for potency >= 0.8' do
128
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.8)
129
+ expect(c.powerful?).to be true
130
+ end
131
+
132
+ it 'returns false for potency < 0.8' do
133
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.7)
134
+ expect(c.powerful?).to be false
135
+ end
136
+ end
137
+
138
+ describe '#inert?' do
139
+ it 'returns true for potency < 0.2' do
140
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.1)
141
+ expect(c.inert?).to be true
142
+ end
143
+
144
+ it 'returns false for potency >= 0.2' do
145
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.2)
146
+ expect(c.inert?).to be false
147
+ end
148
+ end
149
+
150
+ describe '#specific?' do
151
+ it 'returns true for specificity >= 0.7' do
152
+ c = described_class.new(catalyst_type: :insight, domain: :d, specificity: 0.7)
153
+ expect(c.specific?).to be true
154
+ end
155
+
156
+ it 'returns false for specificity < 0.7' do
157
+ c = described_class.new(catalyst_type: :insight, domain: :d, specificity: 0.6)
158
+ expect(c.specific?).to be false
159
+ end
160
+ end
161
+
162
+ describe '#broad?' do
163
+ it 'returns true for specificity < 0.3' do
164
+ c = described_class.new(catalyst_type: :insight, domain: :d, specificity: 0.2)
165
+ expect(c.broad?).to be true
166
+ end
167
+
168
+ it 'returns false for specificity >= 0.3' do
169
+ c = described_class.new(catalyst_type: :insight, domain: :d, specificity: 0.3)
170
+ expect(c.broad?).to be false
171
+ end
172
+ end
173
+
174
+ describe '#potency_label' do
175
+ it 'returns :powerful for potency >= 0.8' do
176
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.9)
177
+ expect(c.potency_label).to eq(:powerful)
178
+ end
179
+
180
+ it 'returns :strong for potency in 0.6...0.8' do
181
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.7)
182
+ expect(c.potency_label).to eq(:strong)
183
+ end
184
+
185
+ it 'returns :moderate for potency in 0.4...0.6' do
186
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.5)
187
+ expect(c.potency_label).to eq(:moderate)
188
+ end
189
+
190
+ it 'returns :weak for potency in 0.2...0.4' do
191
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.3)
192
+ expect(c.potency_label).to eq(:weak)
193
+ end
194
+
195
+ it 'returns :inert for potency < 0.2' do
196
+ c = described_class.new(catalyst_type: :insight, domain: :d, potency: 0.1)
197
+ expect(c.potency_label).to eq(:inert)
198
+ end
199
+ end
200
+
201
+ describe '#to_h' do
202
+ it 'returns a hash with all required keys' do
203
+ h = catalyst.to_h
204
+ expect(h).to include(:id, :catalyst_type, :domain, :potency, :specificity,
205
+ :uses_count, :potency_label, :powerful, :inert,
206
+ :specific, :broad, :created_at)
207
+ end
208
+
209
+ it 'reflects current state' do
210
+ catalyst.catalyze!(:synthesis)
211
+ expect(catalyst.to_h[:uses_count]).to eq(1)
212
+ end
213
+ end
214
+ end