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 +7 -0
- data/Gemfile +11 -0
- data/lex-cognitive-coherence.gemspec +30 -0
- data/lib/legion/extensions/cognitive_coherence/client.rb +24 -0
- data/lib/legion/extensions/cognitive_coherence/helpers/coherence_engine.rb +166 -0
- data/lib/legion/extensions/cognitive_coherence/helpers/constants.rb +31 -0
- data/lib/legion/extensions/cognitive_coherence/helpers/proposition.rb +96 -0
- data/lib/legion/extensions/cognitive_coherence/runners/cognitive_coherence.rb +119 -0
- data/lib/legion/extensions/cognitive_coherence/version.rb +9 -0
- data/lib/legion/extensions/cognitive_coherence.rb +15 -0
- data/spec/legion/extensions/cognitive_coherence/client_spec.rb +17 -0
- data/spec/legion/extensions/cognitive_coherence/runners/cognitive_coherence_spec.rb +267 -0
- data/spec/spec_helper.rb +20 -0
- metadata +73 -0
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,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,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
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/cognitive_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: []
|