lex-cognitive-immunology 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ef857f308ed2f088519c8105a9875d622ea045760b68e44c789bd2903288af0
4
+ data.tar.gz: 71b3c2d6846f87bdce12a781b83f74276550bc796fc72337a3ce9726c5adc952
5
+ SHA512:
6
+ metadata.gz: 0ebd1e41f872b4d8bdffd40f424bb1850bf25f3577810289c3c6c181bc30cebf34a0b1bf81ae07cad37767ed4a41e7d5c8020c6c6fcb04c078b9c24d140a49ae
7
+ data.tar.gz: 7397a05f8c5ba4b258cfda02e66c49a83e8711b596426cbae5d6a048a70a905baa59666e870f2619c38277a1d85ecc8f22cced31b42e9a474e2dcfbf20d694da
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_immunology/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-immunology'
7
+ spec.version = Legion::Extensions::CognitiveImmunology::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Immunology'
12
+ spec.description = 'Cognitive immune system for brain-modeled agentic AI — defense against memetic threats, manipulation, and cognitive viruses'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-immunology'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-immunology'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-immunology'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-immunology'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-immunology/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-immunology.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_immunology/helpers/constants'
4
+ require 'legion/extensions/cognitive_immunology/helpers/threat'
5
+ require 'legion/extensions/cognitive_immunology/helpers/antibody'
6
+ require 'legion/extensions/cognitive_immunology/helpers/immune_engine'
7
+ require 'legion/extensions/cognitive_immunology/runners/cognitive_immunology'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveImmunology
12
+ class Client
13
+ include Runners::CognitiveImmunology
14
+
15
+ def initialize(**)
16
+ @engine = Helpers::ImmuneEngine.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :engine
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveImmunology
8
+ module Helpers
9
+ class Antibody
10
+ attr_reader :id, :tactic, :pattern, :created_at
11
+ attr_accessor :strength, :matches
12
+
13
+ def initialize(tactic:, pattern:, strength: 0.5)
14
+ @id = SecureRandom.uuid
15
+ @tactic = tactic
16
+ @pattern = pattern
17
+ @strength = strength.clamp(0.0, 1.0)
18
+ @matches = 0
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def match!
23
+ @matches += 1
24
+ boost = Constants::RESISTANCE_BOOST / (@matches + 1)
25
+ @strength = (@strength + boost.round(10)).clamp(0.0, 1.0).round(10)
26
+ end
27
+
28
+ def decay!
29
+ @strength = (@strength - Constants::RESISTANCE_DECAY).clamp(0.0, 1.0).round(10)
30
+ end
31
+
32
+ def effective?
33
+ @strength >= 0.3
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ id: @id,
39
+ tactic: @tactic,
40
+ pattern: @pattern,
41
+ strength: @strength,
42
+ matches: @matches,
43
+ effective: effective?,
44
+ created_at: @created_at
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveImmunology
6
+ module Helpers
7
+ module Constants
8
+ DEFAULT_RESISTANCE = 0.5
9
+ RESISTANCE_BOOST = 0.1
10
+ RESISTANCE_DECAY = 0.02
11
+ MAX_THREATS = 500
12
+ MAX_ANTIBODIES = 200
13
+
14
+ THREAT_LABELS = {
15
+ (0.8..) => :critical,
16
+ (0.6...0.8) => :severe,
17
+ (0.4...0.6) => :moderate,
18
+ (0.2...0.4) => :low,
19
+ (..0.2) => :negligible
20
+ }.freeze
21
+
22
+ IMMUNITY_LABELS = {
23
+ (0.8..) => :immune,
24
+ (0.6...0.8) => :resistant,
25
+ (0.4...0.6) => :normal,
26
+ (0.2...0.4) => :vulnerable,
27
+ (..0.2) => :compromised
28
+ }.freeze
29
+
30
+ MANIPULATION_TACTICS = %i[
31
+ authority_appeal emotional_blackmail false_urgency
32
+ social_proof_abuse gaslighting strawman ad_hominem
33
+ sunk_cost_exploit bandwagon fear_mongering
34
+ ].freeze
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveImmunology
6
+ module Helpers
7
+ class ImmuneEngine
8
+ attr_reader :resistance, :inflammatory
9
+
10
+ def initialize
11
+ @threats = {}
12
+ @antibodies = {}
13
+ @resistance = Constants::DEFAULT_RESISTANCE
14
+ @inflammatory = false
15
+ end
16
+
17
+ def detect_threat(source:, tactic:, content_hash:, threat_level: 0.5)
18
+ prune_threats_if_full
19
+
20
+ threat = Threat.new(
21
+ source: source,
22
+ tactic: tactic,
23
+ content_hash: content_hash,
24
+ threat_level: threat_level
25
+ )
26
+
27
+ matched = match_antibodies_for_tactic(tactic)
28
+ matched.each do |ab|
29
+ ab.match!
30
+ reduction = (ab.strength * 0.2).round(10)
31
+ threat.threat_level = (threat.threat_level - reduction).clamp(0.0, 1.0).round(10)
32
+ end
33
+
34
+ @threats[threat.id] = threat
35
+ Legion::Logging.debug "[cognitive_immunology] threat detected: id=#{threat.id} tactic=#{tactic} level=#{threat.threat_level.round(2)}"
36
+ threat
37
+ end
38
+
39
+ def quarantine_threat(threat_id:)
40
+ threat = @threats.fetch(threat_id, nil)
41
+ return { success: false, reason: 'not found' } unless threat
42
+
43
+ threat.quarantine!
44
+ Legion::Logging.info "[cognitive_immunology] quarantined: id=#{threat_id} tactic=#{threat.tactic}"
45
+ { success: true, threat_id: threat_id }
46
+ end
47
+
48
+ def release_threat(threat_id:)
49
+ threat = @threats.fetch(threat_id, nil)
50
+ return { success: false, reason: 'not found' } unless threat
51
+
52
+ threat.release!
53
+ Legion::Logging.debug "[cognitive_immunology] released: id=#{threat_id}"
54
+ { success: true, threat_id: threat_id }
55
+ end
56
+
57
+ def inoculate(threat_id:)
58
+ threat = @threats.fetch(threat_id, nil)
59
+ return { success: false, reason: 'not found' } unless threat
60
+
61
+ threat.expose!
62
+ boost = (Constants::RESISTANCE_BOOST / (threat.exposure_count + 1)).round(10)
63
+ @resistance = (@resistance + boost).clamp(0.0, 1.0).round(10)
64
+ Legion::Logging.debug "[cognitive_immunology] inoculate: id=#{threat_id} exposure=#{threat.exposure_count} resistance=#{@resistance.round(2)}"
65
+ { success: true, threat_id: threat_id, exposure_count: threat.exposure_count, resistance: @resistance }
66
+ end
67
+
68
+ def create_antibody(tactic:, pattern:, strength: 0.5)
69
+ prune_antibodies_if_full
70
+
71
+ ab = Antibody.new(tactic: tactic, pattern: pattern, strength: strength)
72
+ @antibodies[ab.id] = ab
73
+ Legion::Logging.info "[cognitive_immunology] antibody created: id=#{ab.id} tactic=#{tactic} strength=#{strength}"
74
+ ab
75
+ end
76
+
77
+ def scan_for_tactic(tactic:)
78
+ @threats.values.select { |t| t.tactic == tactic }
79
+ end
80
+
81
+ def trigger_inflammatory_response
82
+ @inflammatory = true
83
+ Legion::Logging.warn '[cognitive_immunology] inflammatory response triggered — heightened scrutiny mode'
84
+ { inflammatory: true }
85
+ end
86
+
87
+ def resolve_inflammation
88
+ @inflammatory = false
89
+ Legion::Logging.info '[cognitive_immunology] inflammation resolved — returning to normal scrutiny'
90
+ { inflammatory: false }
91
+ end
92
+
93
+ def overall_immunity
94
+ ab_coverage = antibody_coverage_score
95
+ score = ((@resistance * 0.6) + (ab_coverage * 0.4)).round(10)
96
+ score.clamp(0.0, 1.0)
97
+ end
98
+
99
+ def immunity_label
100
+ score = overall_immunity
101
+ Constants::IMMUNITY_LABELS.find { |range, _| range.cover?(score) }&.last || :compromised
102
+ end
103
+
104
+ def vulnerability_report
105
+ covered_tactics = @antibodies.values.map(&:tactic).uniq
106
+ uncovered = Constants::MANIPULATION_TACTICS.reject { |t| covered_tactics.include?(t) }
107
+ {
108
+ covered: covered_tactics,
109
+ uncovered: uncovered,
110
+ coverage: (covered_tactics.size.to_f / Constants::MANIPULATION_TACTICS.size).round(10)
111
+ }
112
+ end
113
+
114
+ def threat_history(limit: 10)
115
+ @threats.values
116
+ .sort_by(&:created_at)
117
+ .last(limit)
118
+ .map(&:to_h)
119
+ end
120
+
121
+ def decay_all
122
+ @antibodies.each_value(&:decay!)
123
+ @resistance = (@resistance - Constants::RESISTANCE_DECAY).clamp(0.0, 1.0).round(10)
124
+ Legion::Logging.debug "[cognitive_immunology] decay cycle: resistance=#{@resistance.round(2)} antibodies=#{@antibodies.size}"
125
+ { resistance: @resistance, antibodies_decayed: @antibodies.size }
126
+ end
127
+
128
+ def prune_ineffective
129
+ before = @antibodies.size
130
+ @antibodies.select! { |_, ab| ab.effective? }
131
+ pruned = before - @antibodies.size
132
+ Legion::Logging.debug "[cognitive_immunology] pruned #{pruned} ineffective antibodies"
133
+ { pruned: pruned, remaining: @antibodies.size }
134
+ end
135
+
136
+ def to_h
137
+ {
138
+ threat_count: @threats.size,
139
+ quarantined_count: @threats.values.count(&:quarantined),
140
+ antibody_count: @antibodies.size,
141
+ effective_antibody_count: @antibodies.values.count(&:effective?),
142
+ resistance: @resistance,
143
+ inflammatory: @inflammatory,
144
+ overall_immunity: overall_immunity,
145
+ immunity_label: immunity_label
146
+ }
147
+ end
148
+
149
+ private
150
+
151
+ def match_antibodies_for_tactic(tactic)
152
+ @antibodies.values.select { |ab| ab.tactic == tactic && ab.effective? }
153
+ end
154
+
155
+ def antibody_coverage_score
156
+ return 0.0 if @antibodies.empty?
157
+
158
+ effective = @antibodies.values.select(&:effective?)
159
+ return 0.0 if effective.empty?
160
+
161
+ avg_strength = effective.sum(&:strength) / effective.size.to_f
162
+ tactic_coverage = vulnerability_report[:coverage]
163
+ ((avg_strength * 0.5) + (tactic_coverage * 0.5)).round(10)
164
+ end
165
+
166
+ def prune_threats_if_full
167
+ return unless @threats.size >= Constants::MAX_THREATS
168
+
169
+ oldest = @threats.values.min_by(&:created_at)
170
+ @threats.delete(oldest.id) if oldest
171
+ end
172
+
173
+ def prune_antibodies_if_full
174
+ return unless @antibodies.size >= Constants::MAX_ANTIBODIES
175
+
176
+ weakest = @antibodies.values.min_by(&:strength)
177
+ @antibodies.delete(weakest.id) if weakest
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveImmunology
8
+ module Helpers
9
+ class Threat
10
+ attr_reader :id, :source, :tactic, :content_hash, :created_at
11
+ attr_accessor :threat_level, :quarantined, :exposure_count
12
+
13
+ def initialize(source:, tactic:, content_hash:, threat_level: 0.5)
14
+ @id = SecureRandom.uuid
15
+ @source = source
16
+ @tactic = tactic
17
+ @content_hash = content_hash
18
+ @threat_level = threat_level.clamp(0.0, 1.0)
19
+ @quarantined = false
20
+ @exposure_count = 0
21
+ @created_at = Time.now.utc
22
+ end
23
+
24
+ def threat_label
25
+ Constants::THREAT_LABELS.find { |range, _| range.cover?(@threat_level) }&.last || :negligible
26
+ end
27
+
28
+ def quarantine!
29
+ @quarantined = true
30
+ end
31
+
32
+ def release!
33
+ @quarantined = false
34
+ end
35
+
36
+ def expose!
37
+ @exposure_count += 1
38
+ reduction = (0.05 / (@exposure_count + 1)).round(10)
39
+ @threat_level = (@threat_level - reduction).clamp(0.0, 1.0).round(10)
40
+ end
41
+
42
+ def escalate!(amount: 0.1)
43
+ @threat_level = (@threat_level + amount).clamp(0.0, 1.0).round(10)
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ id: @id,
49
+ source: @source,
50
+ tactic: @tactic,
51
+ content_hash: @content_hash,
52
+ threat_level: @threat_level,
53
+ threat_label: threat_label,
54
+ quarantined: @quarantined,
55
+ exposure_count: @exposure_count,
56
+ created_at: @created_at
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveImmunology
6
+ module Runners
7
+ module CognitiveImmunology
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def detect_threat(source:, tactic:, content_hash:, threat_level: 0.5, **)
12
+ threat = engine.detect_threat(source: source, tactic: tactic, content_hash: content_hash, threat_level: threat_level)
13
+ { success: true, threat: threat.to_h }
14
+ end
15
+
16
+ def quarantine_threat(threat_id:, **)
17
+ result = engine.quarantine_threat(threat_id: threat_id)
18
+ result.merge(success: result.fetch(:success, false))
19
+ end
20
+
21
+ def release_threat(threat_id:, **)
22
+ result = engine.release_threat(threat_id: threat_id)
23
+ result.merge(success: result.fetch(:success, false))
24
+ end
25
+
26
+ def inoculate(threat_id:, **)
27
+ engine.inoculate(threat_id: threat_id)
28
+ end
29
+
30
+ def create_antibody(tactic:, pattern:, strength: 0.5, **)
31
+ ab = engine.create_antibody(tactic: tactic, pattern: pattern, strength: strength)
32
+ { success: true, antibody: ab.to_h }
33
+ end
34
+
35
+ def scan_for_tactic(tactic:, **)
36
+ threats = engine.scan_for_tactic(tactic: tactic)
37
+ { success: true, tactic: tactic, threats: threats.map(&:to_h), count: threats.size }
38
+ end
39
+
40
+ def trigger_inflammatory_response(**)
41
+ result = engine.trigger_inflammatory_response
42
+ { success: true }.merge(result)
43
+ end
44
+
45
+ def resolve_inflammation(**)
46
+ result = engine.resolve_inflammation
47
+ { success: true }.merge(result)
48
+ end
49
+
50
+ def overall_immunity(**)
51
+ score = engine.overall_immunity
52
+ { success: true, score: score, label: engine.immunity_label }
53
+ end
54
+
55
+ def vulnerability_report(**)
56
+ report = engine.vulnerability_report
57
+ { success: true }.merge(report)
58
+ end
59
+
60
+ def threat_history(limit: 10, **)
61
+ threats = engine.threat_history(limit: limit)
62
+ { success: true, threats: threats, count: threats.size }
63
+ end
64
+
65
+ def decay_all(**)
66
+ result = engine.decay_all
67
+ { success: true }.merge(result)
68
+ end
69
+
70
+ def prune_ineffective(**)
71
+ result = engine.prune_ineffective
72
+ { success: true }.merge(result)
73
+ end
74
+
75
+ def immune_status(**)
76
+ { success: true }.merge(engine.to_h)
77
+ end
78
+
79
+ private
80
+
81
+ def engine
82
+ @engine ||= Helpers::ImmuneEngine.new
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveImmunology
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_immunology/version'
4
+ require 'legion/extensions/cognitive_immunology/helpers/constants'
5
+ require 'legion/extensions/cognitive_immunology/helpers/threat'
6
+ require 'legion/extensions/cognitive_immunology/helpers/antibody'
7
+ require 'legion/extensions/cognitive_immunology/helpers/immune_engine'
8
+ require 'legion/extensions/cognitive_immunology/runners/cognitive_immunology'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module CognitiveImmunology
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveImmunology::Client do
4
+ let(:client) { described_class.new }
5
+
6
+ it 'responds to detect_threat' do
7
+ expect(client).to respond_to(:detect_threat)
8
+ end
9
+
10
+ it 'responds to quarantine_threat' do
11
+ expect(client).to respond_to(:quarantine_threat)
12
+ end
13
+
14
+ it 'responds to release_threat' do
15
+ expect(client).to respond_to(:release_threat)
16
+ end
17
+
18
+ it 'responds to inoculate' do
19
+ expect(client).to respond_to(:inoculate)
20
+ end
21
+
22
+ it 'responds to create_antibody' do
23
+ expect(client).to respond_to(:create_antibody)
24
+ end
25
+
26
+ it 'responds to scan_for_tactic' do
27
+ expect(client).to respond_to(:scan_for_tactic)
28
+ end
29
+
30
+ it 'responds to trigger_inflammatory_response' do
31
+ expect(client).to respond_to(:trigger_inflammatory_response)
32
+ end
33
+
34
+ it 'responds to resolve_inflammation' do
35
+ expect(client).to respond_to(:resolve_inflammation)
36
+ end
37
+
38
+ it 'responds to overall_immunity' do
39
+ expect(client).to respond_to(:overall_immunity)
40
+ end
41
+
42
+ it 'responds to vulnerability_report' do
43
+ expect(client).to respond_to(:vulnerability_report)
44
+ end
45
+
46
+ it 'responds to threat_history' do
47
+ expect(client).to respond_to(:threat_history)
48
+ end
49
+
50
+ it 'responds to decay_all' do
51
+ expect(client).to respond_to(:decay_all)
52
+ end
53
+
54
+ it 'responds to prune_ineffective' do
55
+ expect(client).to respond_to(:prune_ineffective)
56
+ end
57
+
58
+ it 'responds to immune_status' do
59
+ expect(client).to respond_to(:immune_status)
60
+ end
61
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveImmunology::Helpers::Antibody do
4
+ subject(:antibody) { described_class.new(tactic: :authority_appeal, pattern: 'claimed expert status') }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(antibody.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores tactic' do
12
+ expect(antibody.tactic).to eq(:authority_appeal)
13
+ end
14
+
15
+ it 'stores pattern' do
16
+ expect(antibody.pattern).to eq('claimed expert status')
17
+ end
18
+
19
+ it 'defaults strength to 0.5' do
20
+ expect(antibody.strength).to eq(0.5)
21
+ end
22
+
23
+ it 'defaults matches to 0' do
24
+ expect(antibody.matches).to eq(0)
25
+ end
26
+
27
+ it 'clamps strength above 1.0' do
28
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: 2.0)
29
+ expect(ab.strength).to eq(1.0)
30
+ end
31
+
32
+ it 'clamps strength below 0.0' do
33
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: -1.0)
34
+ expect(ab.strength).to eq(0.0)
35
+ end
36
+ end
37
+
38
+ describe '#match!' do
39
+ it 'increments matches' do
40
+ antibody.match!
41
+ expect(antibody.matches).to eq(1)
42
+ end
43
+
44
+ it 'boosts strength' do
45
+ original = antibody.strength
46
+ antibody.match!
47
+ expect(antibody.strength).to be > original
48
+ end
49
+
50
+ it 'does not exceed 1.0' do
51
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: 1.0)
52
+ ab.match!
53
+ expect(ab.strength).to eq(1.0)
54
+ end
55
+ end
56
+
57
+ describe '#decay!' do
58
+ it 'reduces strength by RESISTANCE_DECAY' do
59
+ original = antibody.strength
60
+ antibody.decay!
61
+ expect(antibody.strength).to be_within(0.001).of(original - Legion::Extensions::CognitiveImmunology::Helpers::Constants::RESISTANCE_DECAY)
62
+ end
63
+
64
+ it 'does not go below 0.0' do
65
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: 0.0)
66
+ ab.decay!
67
+ expect(ab.strength).to eq(0.0)
68
+ end
69
+ end
70
+
71
+ describe '#effective?' do
72
+ it 'returns true when strength >= 0.3' do
73
+ expect(antibody.effective?).to be true
74
+ end
75
+
76
+ it 'returns false when strength < 0.3' do
77
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: 0.1)
78
+ expect(ab.effective?).to be false
79
+ end
80
+
81
+ it 'returns true at exactly 0.3' do
82
+ ab = described_class.new(tactic: :gaslighting, pattern: 'x', strength: 0.3)
83
+ expect(ab.effective?).to be true
84
+ end
85
+ end
86
+
87
+ describe '#to_h' do
88
+ it 'returns a hash with all fields' do
89
+ h = antibody.to_h
90
+ expect(h).to include(:id, :tactic, :pattern, :strength, :matches, :effective, :created_at)
91
+ end
92
+
93
+ it 'includes effective status' do
94
+ h = antibody.to_h
95
+ expect(h[:effective]).to be true
96
+ end
97
+ end
98
+ end