lex-cognitive-coherence 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: ac076a2d936cd58c3c50f7f70bb5e965adeccf4eb20efa211c74537f5b087690
4
+ data.tar.gz: e6824ef6d7b996b078253e9058e46a301e9ada591b712a5908f4abf19005d5a0
5
+ SHA512:
6
+ metadata.gz: 787518a385ff89e680e194041bb5631a3cca34fe998eaf8d0f9d56670205d430cb05cbe0ba4bd390706f4dccce23ae0a76d1a8aade33335eb92662aac0942d93
7
+ data.tar.gz: 12cc72c797be5e285a34beb643629f346bcec4afd0e9f3e80af5d6426571073b332d368af14562200e6058aa5ec1e8524fdd0dbe60d84f95a41d232271942cd6
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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_coherence/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-coherence'
7
+ spec.version = Legion::Extensions::CognitiveCoherence::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Coherence'
12
+ spec.description = "Thagard's coherence theory: constraint satisfaction across beliefs, " \
13
+ 'goals, and evidence for brain-modeled agentic AI'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-coherence'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-coherence'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-coherence'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-coherence'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-coherence/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-coherence.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_coherence/helpers/constants'
4
+ require 'legion/extensions/cognitive_coherence/helpers/proposition'
5
+ require 'legion/extensions/cognitive_coherence/helpers/coherence_engine'
6
+ require 'legion/extensions/cognitive_coherence/runners/cognitive_coherence'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module CognitiveCoherence
11
+ class Client
12
+ include Runners::CognitiveCoherence
13
+
14
+ def initialize(**)
15
+ @engine = Helpers::CoherenceEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCoherence
6
+ module Helpers
7
+ class CoherenceEngine
8
+ include Constants
9
+
10
+ attr_reader :propositions, :history
11
+
12
+ def initialize
13
+ @propositions = {}
14
+ @history = []
15
+ end
16
+
17
+ def add_proposition(content:, domain: :general, acceptance: DEFAULT_ACCEPTANCE)
18
+ return nil if @propositions.size >= MAX_PROPOSITIONS
19
+
20
+ prop = Proposition.new(content: content, domain: domain, acceptance: acceptance)
21
+ @propositions[prop.id] = prop
22
+ record_history(:add_proposition, { id: prop.id, domain: domain })
23
+ prop.id
24
+ end
25
+
26
+ def add_constraint(prop_a_id:, prop_b_id:, constraint_type:, positive: true)
27
+ prop_a = @propositions[prop_a_id]
28
+ prop_b = @propositions[prop_b_id]
29
+ return { success: false, reason: :proposition_not_found } unless prop_a && prop_b
30
+
31
+ unless CONSTRAINT_TYPES.include?(constraint_type)
32
+ return { success: false,
33
+ reason: :invalid_constraint_type }
34
+ end
35
+
36
+ if positive
37
+ prop_a.add_positive_constraint(proposition_id: prop_b_id)
38
+ prop_b.add_positive_constraint(proposition_id: prop_a_id)
39
+ else
40
+ prop_a.add_negative_constraint(proposition_id: prop_b_id)
41
+ prop_b.add_negative_constraint(proposition_id: prop_a_id)
42
+ end
43
+
44
+ record_history(:add_constraint,
45
+ { prop_a: prop_a_id, prop_b: prop_b_id, type: constraint_type, positive: positive })
46
+ { success: true, constraint_type: constraint_type, positive: positive }
47
+ end
48
+
49
+ def compute_coherence(proposition_id:)
50
+ prop = @propositions[proposition_id]
51
+ return 0.0 unless prop
52
+
53
+ positive_sum = prop.positive_constraints.sum do |pid|
54
+ neighbor = @propositions[pid]
55
+ neighbor ? neighbor.acceptance * COHERENCE_WEIGHT : 0.0
56
+ end
57
+
58
+ negative_sum = prop.negative_constraints.sum do |pid|
59
+ neighbor = @propositions[pid]
60
+ neighbor ? neighbor.acceptance * INCOHERENCE_PENALTY : 0.0
61
+ end
62
+
63
+ (prop.acceptance + positive_sum - negative_sum).clamp(0.0, 1.0)
64
+ end
65
+
66
+ def maximize_coherence
67
+ return { success: true, iterations: 0, proposition_count: 0 } if @propositions.empty?
68
+
69
+ @propositions.each_value { |prop| adjust_proposition(prop) }
70
+
71
+ record_history(:maximize_coherence, { overall: overall_coherence })
72
+ {
73
+ success: true,
74
+ iterations: 1,
75
+ proposition_count: @propositions.size,
76
+ overall_coherence: overall_coherence
77
+ }
78
+ end
79
+
80
+ def overall_coherence
81
+ return 0.0 if @propositions.empty?
82
+
83
+ total = @propositions.values.sum { |prop| compute_coherence(proposition_id: prop.id) }
84
+ total / @propositions.size
85
+ end
86
+
87
+ def coherence_label
88
+ val = overall_coherence
89
+ COHERENCE_LABELS.find { |range, _| range.cover?(val) }&.last || :unknown
90
+ end
91
+
92
+ def find_contradictions
93
+ accepted = @propositions.values.select(&:accepted?)
94
+ pairs = []
95
+ accepted.each { |prop| collect_contradiction_pairs(prop, pairs) }
96
+ pairs
97
+ end
98
+
99
+ def partition
100
+ result = { accepted: [], rejected: [], undecided: [] }
101
+ @propositions.each_value { |prop| result[prop.state] << prop.to_h }
102
+ result
103
+ end
104
+
105
+ def by_domain(domain:)
106
+ @propositions.values.select { |prop| prop.domain == domain }.map(&:to_h)
107
+ end
108
+
109
+ def decay_all
110
+ count = 0
111
+ @propositions.each_value do |prop|
112
+ next if prop.acceptance == DEFAULT_ACCEPTANCE
113
+
114
+ delta = (DEFAULT_ACCEPTANCE - prop.acceptance) * DECAY_RATE
115
+ prop.adjust_acceptance(amount: delta) unless delta.abs < 0.0001
116
+ count += 1
117
+ end
118
+ { success: true, decayed_count: count }
119
+ end
120
+
121
+ def to_h
122
+ {
123
+ proposition_count: @propositions.size,
124
+ overall_coherence: overall_coherence,
125
+ coherence_label: coherence_label,
126
+ partition: partition,
127
+ history_size: @history.size
128
+ }
129
+ end
130
+
131
+ private
132
+
133
+ def adjust_proposition(prop)
134
+ positive_pull = prop.positive_constraints.sum do |pid|
135
+ neighbor = @propositions[pid]
136
+ neighbor ? neighbor.acceptance * COHERENCE_WEIGHT : 0.0
137
+ end
138
+
139
+ negative_push = prop.negative_constraints.sum do |pid|
140
+ neighbor = @propositions[pid]
141
+ neighbor ? neighbor.acceptance * INCOHERENCE_PENALTY : 0.0
142
+ end
143
+
144
+ delta = positive_pull - negative_push
145
+ prop.adjust_acceptance(amount: delta) unless delta.abs < 0.001
146
+ end
147
+
148
+ def collect_contradiction_pairs(prop, pairs)
149
+ prop.negative_constraints.each do |neg_id|
150
+ neighbor = @propositions[neg_id]
151
+ next unless neighbor&.accepted?
152
+ next if pairs.any? { |pair| pair[:prop_b] == prop.id && pair[:prop_a] == neg_id }
153
+
154
+ pairs << { prop_a: prop.id, prop_b: neg_id }
155
+ end
156
+ end
157
+
158
+ def record_history(event, data)
159
+ @history << { event: event, data: data, at: Time.now.utc }
160
+ @history.shift while @history.size > MAX_HISTORY
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCoherence
6
+ module Helpers
7
+ module Constants
8
+ MAX_PROPOSITIONS = 200
9
+ MAX_CONSTRAINTS = 500
10
+ MAX_HISTORY = 300
11
+ DEFAULT_ACCEPTANCE = 0.5
12
+ ACCEPTANCE_THRESHOLD = 0.6
13
+ COHERENCE_WEIGHT = 0.1
14
+ INCOHERENCE_PENALTY = 0.15
15
+ DECAY_RATE = 0.01
16
+
17
+ CONSTRAINT_TYPES = %i[explanatory deductive analogical perceptual conceptual deliberative].freeze
18
+ PROPOSITION_STATES = %i[accepted rejected undecided].freeze
19
+
20
+ COHERENCE_LABELS = {
21
+ (0.8..) => :highly_coherent,
22
+ (0.6...0.8) => :coherent,
23
+ (0.4...0.6) => :mixed,
24
+ (0.2...0.4) => :incoherent,
25
+ (..0.2) => :contradictory
26
+ }.freeze
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveCoherence
8
+ module Helpers
9
+ class Proposition
10
+ include Constants
11
+
12
+ attr_reader :id, :content, :domain, :acceptance,
13
+ :positive_constraints, :negative_constraints,
14
+ :evidence_count, :created_at, :updated_at
15
+
16
+ def initialize(content:, domain: :general, acceptance: DEFAULT_ACCEPTANCE)
17
+ @id = SecureRandom.uuid
18
+ @content = content
19
+ @domain = domain
20
+ @acceptance = acceptance.clamp(0.0, 1.0)
21
+ @positive_constraints = []
22
+ @negative_constraints = []
23
+ @evidence_count = 0
24
+ @created_at = Time.now.utc
25
+ @updated_at = Time.now.utc
26
+ end
27
+
28
+ def state
29
+ if @acceptance >= ACCEPTANCE_THRESHOLD
30
+ :accepted
31
+ elsif @acceptance < (1.0 - ACCEPTANCE_THRESHOLD)
32
+ :rejected
33
+ else
34
+ :undecided
35
+ end
36
+ end
37
+
38
+ def accepted?
39
+ state == :accepted
40
+ end
41
+
42
+ def rejected?
43
+ state == :rejected
44
+ end
45
+
46
+ def undecided?
47
+ state == :undecided
48
+ end
49
+
50
+ def add_positive_constraint(proposition_id:)
51
+ return false if @positive_constraints.include?(proposition_id)
52
+
53
+ @positive_constraints << proposition_id
54
+ @updated_at = Time.now.utc
55
+ true
56
+ end
57
+
58
+ def add_negative_constraint(proposition_id:)
59
+ return false if @negative_constraints.include?(proposition_id)
60
+
61
+ @negative_constraints << proposition_id
62
+ @updated_at = Time.now.utc
63
+ true
64
+ end
65
+
66
+ def adjust_acceptance(amount:)
67
+ @acceptance = (@acceptance + amount).clamp(0.0, 1.0)
68
+ @updated_at = Time.now.utc
69
+ @acceptance
70
+ end
71
+
72
+ def add_evidence
73
+ @evidence_count += 1
74
+ @updated_at = Time.now.utc
75
+ @evidence_count
76
+ end
77
+
78
+ def to_h
79
+ {
80
+ id: @id,
81
+ content: @content,
82
+ domain: @domain,
83
+ acceptance: @acceptance,
84
+ state: state,
85
+ positive_constraints: @positive_constraints.dup,
86
+ negative_constraints: @negative_constraints.dup,
87
+ evidence_count: @evidence_count,
88
+ created_at: @created_at,
89
+ updated_at: @updated_at
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCoherence
6
+ module Runners
7
+ module CognitiveCoherence
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def add_coherence_proposition(content:, domain: :general, acceptance: Helpers::Constants::DEFAULT_ACCEPTANCE,
12
+ **)
13
+ return { success: false, reason: :missing_content } if content.nil? || content.empty?
14
+
15
+ prop_id = engine.add_proposition(content: content, domain: domain, acceptance: acceptance)
16
+ if prop_id
17
+ Legion::Logging.debug "[cognitive_coherence] add_proposition domain=#{domain} id=#{prop_id[0..7]}"
18
+ { success: true, proposition_id: prop_id, domain: domain, acceptance: acceptance }
19
+ else
20
+ Legion::Logging.warn '[cognitive_coherence] add_proposition failed: max propositions reached'
21
+ { success: false, reason: :max_propositions_reached }
22
+ end
23
+ end
24
+
25
+ def add_coherence_constraint(prop_a_id:, prop_b_id:, constraint_type:, positive: true, **)
26
+ unless Helpers::Constants::CONSTRAINT_TYPES.include?(constraint_type)
27
+ return { success: false, reason: :invalid_constraint_type,
28
+ valid_types: Helpers::Constants::CONSTRAINT_TYPES }
29
+ end
30
+
31
+ result = engine.add_constraint(
32
+ prop_a_id: prop_a_id,
33
+ prop_b_id: prop_b_id,
34
+ constraint_type: constraint_type,
35
+ positive: positive
36
+ )
37
+
38
+ Legion::Logging.debug "[cognitive_coherence] add_constraint type=#{constraint_type} " \
39
+ "positive=#{positive} success=#{result[:success]}"
40
+ result
41
+ end
42
+
43
+ def compute_proposition_coherence(proposition_id:, **)
44
+ score = engine.compute_coherence(proposition_id: proposition_id)
45
+ prop = engine.propositions[proposition_id]
46
+
47
+ unless prop
48
+ Legion::Logging.debug "[cognitive_coherence] compute_coherence: #{proposition_id[0..7]} not found"
49
+ return { success: false, reason: :not_found }
50
+ end
51
+
52
+ Legion::Logging.debug '[cognitive_coherence] compute_coherence ' \
53
+ "id=#{proposition_id[0..7]} score=#{score.round(3)}"
54
+ { success: true, proposition_id: proposition_id, coherence_score: score, state: prop.state }
55
+ end
56
+
57
+ def maximize_coherence(**)
58
+ result = engine.maximize_coherence
59
+ overall = result[:overall_coherence]&.round(3)
60
+ Legion::Logging.info '[cognitive_coherence] maximize_coherence ' \
61
+ "overall=#{overall} props=#{result[:proposition_count]}"
62
+ result
63
+ end
64
+
65
+ def find_contradictions(**)
66
+ pairs = engine.find_contradictions
67
+ Legion::Logging.debug "[cognitive_coherence] find_contradictions count=#{pairs.size}"
68
+ { success: true, contradictions: pairs, count: pairs.size }
69
+ end
70
+
71
+ def coherence_partition(**)
72
+ result = engine.partition
73
+ totals = result.transform_values(&:size)
74
+ Legion::Logging.debug "[cognitive_coherence] partition accepted=#{totals[:accepted]} " \
75
+ "rejected=#{totals[:rejected]} undecided=#{totals[:undecided]}"
76
+ { success: true, partition: result, counts: totals }
77
+ end
78
+
79
+ def update_cognitive_coherence(**)
80
+ coherence_result = engine.maximize_coherence
81
+ decay_result = engine.decay_all
82
+
83
+ overall = coherence_result[:overall_coherence]&.round(3)
84
+ Legion::Logging.info "[cognitive_coherence] update overall=#{overall} " \
85
+ "decayed=#{decay_result[:decayed_count]}"
86
+ {
87
+ success: true,
88
+ overall_coherence: coherence_result[:overall_coherence],
89
+ coherence_label: engine.coherence_label,
90
+ decayed_count: decay_result[:decayed_count]
91
+ }
92
+ end
93
+
94
+ def cognitive_coherence_stats(**)
95
+ part = engine.partition
96
+ counts = part.transform_values(&:size)
97
+ Legion::Logging.debug "[cognitive_coherence] stats propositions=#{engine.propositions.size} " \
98
+ "coherence=#{engine.overall_coherence.round(3)}"
99
+ {
100
+ success: true,
101
+ proposition_count: engine.propositions.size,
102
+ overall_coherence: engine.overall_coherence,
103
+ coherence_label: engine.coherence_label,
104
+ partition_counts: counts,
105
+ contradiction_count: engine.find_contradictions.size,
106
+ history_size: engine.history.size
107
+ }
108
+ end
109
+
110
+ private
111
+
112
+ def engine
113
+ @engine ||= Helpers::CoherenceEngine.new
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveCoherence
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_coherence/version'
4
+ require 'legion/extensions/cognitive_coherence/helpers/constants'
5
+ require 'legion/extensions/cognitive_coherence/helpers/proposition'
6
+ require 'legion/extensions/cognitive_coherence/helpers/coherence_engine'
7
+ require 'legion/extensions/cognitive_coherence/runners/cognitive_coherence'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveCoherence
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_coherence/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveCoherence::Client do
6
+ it 'responds to all runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:add_coherence_proposition)
9
+ expect(client).to respond_to(:add_coherence_constraint)
10
+ expect(client).to respond_to(:compute_proposition_coherence)
11
+ expect(client).to respond_to(:maximize_coherence)
12
+ expect(client).to respond_to(:find_contradictions)
13
+ expect(client).to respond_to(:coherence_partition)
14
+ expect(client).to respond_to(:update_cognitive_coherence)
15
+ expect(client).to respond_to(:cognitive_coherence_stats)
16
+ end
17
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_coherence/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveCoherence::Runners::CognitiveCoherence do
6
+ let(:client) { Legion::Extensions::CognitiveCoherence::Client.new }
7
+
8
+ describe '#add_coherence_proposition' do
9
+ it 'adds a proposition and returns its id' do
10
+ result = client.add_coherence_proposition(content: 'The sky is blue', domain: :perception)
11
+ expect(result[:success]).to be true
12
+ expect(result[:proposition_id]).to match(/\A[0-9a-f-]{36}\z/)
13
+ expect(result[:domain]).to eq(:perception)
14
+ end
15
+
16
+ it 'uses default acceptance when none provided' do
17
+ result = client.add_coherence_proposition(content: 'Water is wet')
18
+ expect(result[:acceptance]).to eq(Legion::Extensions::CognitiveCoherence::Helpers::Constants::DEFAULT_ACCEPTANCE)
19
+ end
20
+
21
+ it 'accepts a custom acceptance value' do
22
+ result = client.add_coherence_proposition(content: 'Fire is hot', acceptance: 0.9)
23
+ expect(result[:acceptance]).to eq(0.9)
24
+ end
25
+
26
+ it 'returns failure for empty content' do
27
+ result = client.add_coherence_proposition(content: '')
28
+ expect(result[:success]).to be false
29
+ expect(result[:reason]).to eq(:missing_content)
30
+ end
31
+
32
+ it 'accepts extra keyword arguments via splat' do
33
+ result = client.add_coherence_proposition(content: 'test', extra_param: 'ignored')
34
+ expect(result[:success]).to be true
35
+ end
36
+ end
37
+
38
+ describe '#add_coherence_constraint' do
39
+ let(:prop_a_id) { client.add_coherence_proposition(content: 'A')[:proposition_id] }
40
+ let(:prop_b_id) { client.add_coherence_proposition(content: 'B')[:proposition_id] }
41
+
42
+ it 'adds a positive coherence constraint between two propositions' do
43
+ result = client.add_coherence_constraint(
44
+ prop_a_id: prop_a_id,
45
+ prop_b_id: prop_b_id,
46
+ constraint_type: :explanatory,
47
+ positive: true
48
+ )
49
+ expect(result[:success]).to be true
50
+ expect(result[:constraint_type]).to eq(:explanatory)
51
+ expect(result[:positive]).to be true
52
+ end
53
+
54
+ it 'adds a negative (incoherence) constraint' do
55
+ result = client.add_coherence_constraint(
56
+ prop_a_id: prop_a_id,
57
+ prop_b_id: prop_b_id,
58
+ constraint_type: :deductive,
59
+ positive: false
60
+ )
61
+ expect(result[:success]).to be true
62
+ expect(result[:positive]).to be false
63
+ end
64
+
65
+ it 'rejects invalid constraint types' do
66
+ result = client.add_coherence_constraint(
67
+ prop_a_id: prop_a_id,
68
+ prop_b_id: prop_b_id,
69
+ constraint_type: :invalid_type
70
+ )
71
+ expect(result[:success]).to be false
72
+ expect(result[:reason]).to eq(:invalid_constraint_type)
73
+ end
74
+
75
+ it 'returns failure when proposition not found' do
76
+ result = client.add_coherence_constraint(
77
+ prop_a_id: 'nonexistent-id',
78
+ prop_b_id: prop_b_id,
79
+ constraint_type: :explanatory
80
+ )
81
+ expect(result[:success]).to be false
82
+ expect(result[:reason]).to eq(:proposition_not_found)
83
+ end
84
+ end
85
+
86
+ describe '#compute_proposition_coherence' do
87
+ it 'computes coherence score for an existing proposition' do
88
+ result = client.add_coherence_proposition(content: 'test proposition', acceptance: 0.7)
89
+ prop_id = result[:proposition_id]
90
+
91
+ coherence = client.compute_proposition_coherence(proposition_id: prop_id)
92
+ expect(coherence[:success]).to be true
93
+ expect(coherence[:coherence_score]).to be_a(Float)
94
+ expect(coherence[:coherence_score]).to be_between(0.0, 1.0)
95
+ end
96
+
97
+ it 'returns not_found for missing proposition' do
98
+ result = client.compute_proposition_coherence(proposition_id: 'no-such-id')
99
+ expect(result[:success]).to be false
100
+ expect(result[:reason]).to eq(:not_found)
101
+ end
102
+
103
+ it 'increases coherence score when positive constraints are linked' do
104
+ prop_a = client.add_coherence_proposition(content: 'A', acceptance: 0.8)[:proposition_id]
105
+ prop_b = client.add_coherence_proposition(content: 'B', acceptance: 0.8)[:proposition_id]
106
+
107
+ score_before = client.compute_proposition_coherence(proposition_id: prop_a)[:coherence_score]
108
+
109
+ client.add_coherence_constraint(
110
+ prop_a_id: prop_a,
111
+ prop_b_id: prop_b,
112
+ constraint_type: :explanatory,
113
+ positive: true
114
+ )
115
+
116
+ score_after = client.compute_proposition_coherence(proposition_id: prop_a)[:coherence_score]
117
+ expect(score_after).to be > score_before
118
+ end
119
+
120
+ it 'decreases coherence score when negative constraints are linked' do
121
+ prop_a = client.add_coherence_proposition(content: 'A', acceptance: 0.8)[:proposition_id]
122
+ prop_b = client.add_coherence_proposition(content: 'B', acceptance: 0.9)[:proposition_id]
123
+
124
+ score_before = client.compute_proposition_coherence(proposition_id: prop_a)[:coherence_score]
125
+
126
+ client.add_coherence_constraint(
127
+ prop_a_id: prop_a,
128
+ prop_b_id: prop_b,
129
+ constraint_type: :deductive,
130
+ positive: false
131
+ )
132
+
133
+ score_after = client.compute_proposition_coherence(proposition_id: prop_a)[:coherence_score]
134
+ expect(score_after).to be < score_before
135
+ end
136
+ end
137
+
138
+ describe '#maximize_coherence' do
139
+ it 'returns success with iteration info when propositions exist' do
140
+ client.add_coherence_proposition(content: 'p1', acceptance: 0.7)
141
+ client.add_coherence_proposition(content: 'p2', acceptance: 0.3)
142
+
143
+ result = client.maximize_coherence
144
+ expect(result[:success]).to be true
145
+ expect(result[:proposition_count]).to eq(2)
146
+ expect(result[:overall_coherence]).to be_a(Float)
147
+ end
148
+
149
+ it 'returns success with zero iterations when no propositions' do
150
+ result = client.maximize_coherence
151
+ expect(result[:success]).to be true
152
+ expect(result[:iterations]).to eq(0)
153
+ end
154
+
155
+ it 'adjusts propositions toward coherent neighbors' do
156
+ prop_a = client.add_coherence_proposition(content: 'A', acceptance: 0.9)[:proposition_id]
157
+ prop_b = client.add_coherence_proposition(content: 'B', acceptance: 0.1)[:proposition_id]
158
+
159
+ client.add_coherence_constraint(
160
+ prop_a_id: prop_a,
161
+ prop_b_id: prop_b,
162
+ constraint_type: :explanatory,
163
+ positive: true
164
+ )
165
+
166
+ client.maximize_coherence
167
+
168
+ prop_b_coherence = client.compute_proposition_coherence(proposition_id: prop_b)
169
+ expect(prop_b_coherence[:coherence_score]).to be > 0.1
170
+ end
171
+ end
172
+
173
+ describe '#find_contradictions' do
174
+ it 'returns empty list when no contradictions exist' do
175
+ client.add_coherence_proposition(content: 'p1', acceptance: 0.8)
176
+ result = client.find_contradictions
177
+ expect(result[:success]).to be true
178
+ expect(result[:contradictions]).to be_empty
179
+ expect(result[:count]).to eq(0)
180
+ end
181
+
182
+ it 'detects contradictory accepted propositions' do
183
+ prop_a = client.add_coherence_proposition(content: 'A', acceptance: 0.9)[:proposition_id]
184
+ prop_b = client.add_coherence_proposition(content: 'B', acceptance: 0.9)[:proposition_id]
185
+
186
+ client.add_coherence_constraint(
187
+ prop_a_id: prop_a,
188
+ prop_b_id: prop_b,
189
+ constraint_type: :deductive,
190
+ positive: false
191
+ )
192
+
193
+ result = client.find_contradictions
194
+ expect(result[:count]).to eq(1)
195
+ expect(result[:contradictions].first).to include(:prop_a, :prop_b)
196
+ end
197
+
198
+ it 'does not flag contradictions when one proposition is rejected' do
199
+ prop_a = client.add_coherence_proposition(content: 'A', acceptance: 0.9)[:proposition_id]
200
+ prop_b = client.add_coherence_proposition(content: 'B', acceptance: 0.1)[:proposition_id]
201
+
202
+ client.add_coherence_constraint(
203
+ prop_a_id: prop_a,
204
+ prop_b_id: prop_b,
205
+ constraint_type: :deductive,
206
+ positive: false
207
+ )
208
+
209
+ result = client.find_contradictions
210
+ expect(result[:count]).to eq(0)
211
+ end
212
+ end
213
+
214
+ describe '#coherence_partition' do
215
+ it 'partitions propositions by state' do
216
+ client.add_coherence_proposition(content: 'accepted', acceptance: 0.9)
217
+ client.add_coherence_proposition(content: 'rejected', acceptance: 0.1)
218
+ client.add_coherence_proposition(content: 'undecided', acceptance: 0.5)
219
+
220
+ result = client.coherence_partition
221
+ expect(result[:success]).to be true
222
+ expect(result[:counts][:accepted]).to be >= 1
223
+ expect(result[:counts][:rejected]).to be >= 1
224
+ expect(result[:counts][:undecided]).to be >= 1
225
+ end
226
+
227
+ it 'returns partition with proposition hashes' do
228
+ client.add_coherence_proposition(content: 'high', acceptance: 0.8)
229
+ result = client.coherence_partition
230
+ expect(result[:partition][:accepted].first).to include(:id, :content, :acceptance, :state)
231
+ end
232
+ end
233
+
234
+ describe '#update_cognitive_coherence' do
235
+ it 'runs maximize and decay, returning coherence info' do
236
+ client.add_coherence_proposition(content: 'p1', acceptance: 0.8)
237
+ client.add_coherence_proposition(content: 'p2', acceptance: 0.2)
238
+
239
+ result = client.update_cognitive_coherence
240
+ expect(result[:success]).to be true
241
+ expect(result[:overall_coherence]).to be_a(Float)
242
+ expect(result[:coherence_label]).to be_a(Symbol)
243
+ expect(result).to have_key(:decayed_count)
244
+ end
245
+ end
246
+
247
+ describe '#cognitive_coherence_stats' do
248
+ it 'returns stats summary' do
249
+ client.add_coherence_proposition(content: 'stat test', acceptance: 0.7)
250
+
251
+ result = client.cognitive_coherence_stats
252
+ expect(result[:success]).to be true
253
+ expect(result[:proposition_count]).to eq(1)
254
+ expect(result[:overall_coherence]).to be_a(Float)
255
+ expect(result[:coherence_label]).to be_a(Symbol)
256
+ expect(result[:partition_counts]).to be_a(Hash)
257
+ expect(result[:contradiction_count]).to be_a(Integer)
258
+ expect(result[:history_size]).to be_a(Integer)
259
+ end
260
+
261
+ it 'reports zero when no propositions' do
262
+ result = client.cognitive_coherence_stats
263
+ expect(result[:proposition_count]).to eq(0)
264
+ expect(result[:overall_coherence]).to eq(0.0)
265
+ end
266
+ end
267
+ end
@@ -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_coherence'
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
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-cognitive-coherence
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: 'Thagard''s coherence theory: constraint satisfaction across beliefs,
27
+ goals, and evidence for brain-modeled agentic AI'
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-cognitive-coherence.gemspec
36
+ - lib/legion/extensions/cognitive_coherence.rb
37
+ - lib/legion/extensions/cognitive_coherence/client.rb
38
+ - lib/legion/extensions/cognitive_coherence/helpers/coherence_engine.rb
39
+ - lib/legion/extensions/cognitive_coherence/helpers/constants.rb
40
+ - lib/legion/extensions/cognitive_coherence/helpers/proposition.rb
41
+ - lib/legion/extensions/cognitive_coherence/runners/cognitive_coherence.rb
42
+ - lib/legion/extensions/cognitive_coherence/version.rb
43
+ - spec/legion/extensions/cognitive_coherence/client_spec.rb
44
+ - spec/legion/extensions/cognitive_coherence/runners/cognitive_coherence_spec.rb
45
+ - spec/spec_helper.rb
46
+ homepage: https://github.com/LegionIO/lex-cognitive-coherence
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-coherence
51
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-coherence
52
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-coherence
53
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-coherence
54
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-coherence/issues
55
+ rubygems_mfa_required: 'true'
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '3.4'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.6.9
71
+ specification_version: 4
72
+ summary: LEX Cognitive Coherence
73
+ test_files: []