lex-causal-reasoning 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: 88afa7864d6dde283f8596ad1e51ad4a3bc78979213b93f17586cb4e9b7bece6
4
+ data.tar.gz: 6c079e57fb21b832e38292d99ad9ba065e34f1118cdfa45f96ecf035af316fce
5
+ SHA512:
6
+ metadata.gz: 71ca379df8257700e646032095624246f273063884224e0f74718bf13b61eaa6aecaa499a07f88b799997f4706a8e1280f2aac672efe56fcb54141b2dbdcee8f
7
+ data.tar.gz: d5596c7ea7ef37f4f3c83aaf5787a353cc0b75272b83ce80a85c535cceb6d520d2926b312931baa875b89d2578ff38b6d6e2f817f06e4ede3b7c7e7247a0a3a7
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/causal_reasoning/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-causal-reasoning'
7
+ spec.version = Legion::Extensions::CausalReasoning::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Causal Reasoning'
12
+ spec.description = 'Causal inference engine for brain-modeled agentic AI — do-calculus and causal graphs'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-causal-reasoning'
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-causal-reasoning'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-causal-reasoning'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-causal-reasoning'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-causal-reasoning/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-causal-reasoning.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/causal_reasoning/helpers/constants'
4
+ require 'legion/extensions/causal_reasoning/helpers/causal_edge'
5
+ require 'legion/extensions/causal_reasoning/helpers/causal_graph'
6
+ require 'legion/extensions/causal_reasoning/runners/causal_reasoning'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module CausalReasoning
11
+ class Client
12
+ include Runners::CausalReasoning
13
+
14
+ def initialize(graph: nil, **)
15
+ @graph = graph
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CausalReasoning
8
+ module Helpers
9
+ class CausalEdge
10
+ include Constants
11
+
12
+ attr_reader :id, :cause, :effect, :edge_type, :domain, :evidence_count, :created_at, :updated_at, :strength
13
+
14
+ def initialize(cause:, effect:, edge_type:, domain: :general, strength: Constants::DEFAULT_STRENGTH)
15
+ @id = SecureRandom.uuid
16
+ @cause = cause
17
+ @effect = effect
18
+ @edge_type = edge_type
19
+ @domain = domain
20
+ @strength = strength.clamp(Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING)
21
+ @evidence_count = 0
22
+ @created_at = Time.now.utc
23
+ @updated_at = Time.now.utc
24
+ end
25
+
26
+ def add_evidence
27
+ @evidence_count += 1
28
+ @strength = (@strength + Constants::REINFORCEMENT_RATE).clamp(
29
+ Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING
30
+ )
31
+ @updated_at = Time.now.utc
32
+ self
33
+ end
34
+
35
+ def remove_evidence
36
+ @evidence_count = [@evidence_count - 1, 0].max
37
+ @strength = (@strength - Constants::REINFORCEMENT_RATE).clamp(
38
+ Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING
39
+ )
40
+ @updated_at = Time.now.utc
41
+ self
42
+ end
43
+
44
+ def reinforce(amount: Constants::REINFORCEMENT_RATE)
45
+ @strength = (@strength + amount).clamp(
46
+ Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING
47
+ )
48
+ @updated_at = Time.now.utc
49
+ self
50
+ end
51
+
52
+ def weaken(amount: Constants::REINFORCEMENT_RATE)
53
+ @strength = (@strength - amount).clamp(
54
+ Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING
55
+ )
56
+ @updated_at = Time.now.utc
57
+ self
58
+ end
59
+
60
+ def decay
61
+ @strength = (@strength - Constants::DECAY_RATE).clamp(
62
+ Constants::STRENGTH_FLOOR, Constants::STRENGTH_CEILING
63
+ )
64
+ @updated_at = Time.now.utc
65
+ self
66
+ end
67
+
68
+ def confident?
69
+ @strength >= Constants::CAUSAL_THRESHOLD &&
70
+ @evidence_count >= Constants::EVIDENCE_THRESHOLD
71
+ end
72
+
73
+ def confidence_label
74
+ match = Constants::CONFIDENCE_LABELS.find { |range, _| range.cover?(@strength) }
75
+ match ? match[1] : :speculative
76
+ end
77
+
78
+ def to_h
79
+ {
80
+ id: @id,
81
+ cause: @cause,
82
+ effect: @effect,
83
+ edge_type: @edge_type,
84
+ domain: @domain,
85
+ strength: @strength,
86
+ evidence_count: @evidence_count,
87
+ confident: confident?,
88
+ label: confidence_label,
89
+ created_at: @created_at,
90
+ updated_at: @updated_at
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CausalReasoning
6
+ module Helpers
7
+ class CausalGraph
8
+ def initialize
9
+ @variables = {}
10
+ @edges = {}
11
+ @edge_index = {}
12
+ end
13
+
14
+ def add_variable(name:, domain: :general)
15
+ return nil if @variables.key?(name)
16
+ return nil if @variables.size >= Constants::MAX_VARIABLES
17
+
18
+ @variables[name] = { name: name, domain: domain, added_at: Time.now.utc }
19
+ end
20
+
21
+ def variable_exists?(name)
22
+ @variables.key?(name)
23
+ end
24
+
25
+ def add_edge(cause:, effect:, edge_type:, domain: :general, strength: Constants::DEFAULT_STRENGTH)
26
+ return nil if @edges.size >= Constants::MAX_EDGES
27
+ return nil unless Constants::EDGE_TYPES.include?(edge_type)
28
+
29
+ add_variable(name: cause, domain: domain)
30
+ add_variable(name: effect, domain: domain)
31
+
32
+ edge = CausalEdge.new(cause: cause, effect: effect,
33
+ edge_type: edge_type, domain: domain, strength: strength)
34
+ @edges[edge.id] = edge
35
+ @edge_index[cause] ||= []
36
+ @edge_index[cause] << edge.id
37
+ edge
38
+ end
39
+
40
+ def remove_edge(edge_id:)
41
+ edge = @edges.delete(edge_id)
42
+ return nil unless edge
43
+
44
+ @edge_index[edge.cause]&.delete(edge_id)
45
+ edge
46
+ end
47
+
48
+ def causes_of(variable:)
49
+ @edges.values.select { |e| e.effect == variable }
50
+ end
51
+
52
+ def effects_of(variable:)
53
+ ids = @edge_index.fetch(variable, [])
54
+ ids.filter_map { |id| @edges[id] }
55
+ end
56
+
57
+ def causal_chain(from:, to:, max_depth: 5)
58
+ return [] if from == to
59
+
60
+ queue = [[from, [from]]]
61
+ visited = { from => true }
62
+ paths = []
63
+
64
+ until queue.empty?
65
+ current, path = queue.shift
66
+ next if path.size > max_depth + 1
67
+
68
+ effects_of(variable: current).each do |edge|
69
+ neighbor = edge.effect
70
+ new_path = path + [neighbor]
71
+
72
+ if neighbor == to
73
+ paths << new_path
74
+ next
75
+ end
76
+
77
+ next if visited[neighbor]
78
+
79
+ visited[neighbor] = true
80
+ queue << [neighbor, new_path]
81
+ end
82
+ end
83
+
84
+ paths
85
+ end
86
+
87
+ def intervene(variable:, value:)
88
+ downstream = []
89
+ queue = [variable]
90
+ visited = { variable => true }
91
+
92
+ until queue.empty?
93
+ current = queue.shift
94
+ effects_of(variable: current).each do |edge|
95
+ neighbor = edge.effect
96
+ downstream << { variable: neighbor, via_edge: edge.id, edge_type: edge.edge_type }
97
+ next if visited[neighbor]
98
+
99
+ visited[neighbor] = true
100
+ queue << neighbor
101
+ end
102
+ end
103
+
104
+ { intervention: variable, value: value, downstream_effects: downstream }
105
+ end
106
+
107
+ def observe(variable:, value:, evidence:)
108
+ affected = causes_of(variable: variable) + effects_of(variable: variable)
109
+ affected.each { |edge| edge.add_evidence if evidence }
110
+ { variable: variable, value: value, edges_updated: affected.size }
111
+ end
112
+
113
+ def confounders(var_a:, var_b:)
114
+ ancestors_a = ancestors_of(var_a)
115
+ ancestors_b = ancestors_of(var_b)
116
+ common = ancestors_a & ancestors_b
117
+ common.reject { |v| v == var_a || v == var_b }
118
+ end
119
+
120
+ def add_evidence(edge_id:)
121
+ edge = @edges[edge_id]
122
+ return nil unless edge
123
+
124
+ edge.add_evidence
125
+ end
126
+
127
+ def remove_evidence(edge_id:)
128
+ edge = @edges[edge_id]
129
+ return nil unless edge
130
+
131
+ edge.remove_evidence
132
+ end
133
+
134
+ def confident_edges
135
+ @edges.values.select(&:confident?)
136
+ end
137
+
138
+ def by_domain(domain:)
139
+ @edges.values.select { |e| e.domain == domain }
140
+ end
141
+
142
+ def by_type(type:)
143
+ @edges.values.select { |e| e.edge_type == type }
144
+ end
145
+
146
+ def decay_all
147
+ @edges.each_value(&:decay)
148
+ @edges.size
149
+ end
150
+
151
+ def prune_weak
152
+ weak_ids = @edges.select { |_, e| e.strength <= Constants::STRENGTH_FLOOR }.keys
153
+ weak_ids.each { |id| remove_edge(edge_id: id) }
154
+ weak_ids.size
155
+ end
156
+
157
+ def to_h
158
+ {
159
+ variables: @variables.size,
160
+ edges: @edges.size,
161
+ confident_edges: confident_edges.size,
162
+ edge_types: Constants::EDGE_TYPES.to_h { |t| [t, by_type(type: t).size] }
163
+ }
164
+ end
165
+
166
+ private
167
+
168
+ def ancestors_of(variable, visited = {})
169
+ return [] if visited[variable]
170
+
171
+ visited[variable] = true
172
+ direct_causes = causes_of(variable: variable).map(&:cause)
173
+ indirect = direct_causes.flat_map { |c| ancestors_of(c, visited) }
174
+ (direct_causes + indirect).uniq
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CausalReasoning
6
+ module Helpers
7
+ module Constants
8
+ MAX_VARIABLES = 200
9
+ MAX_EDGES = 500
10
+ MAX_HISTORY = 300
11
+
12
+ DEFAULT_STRENGTH = 0.5
13
+ STRENGTH_FLOOR = 0.05
14
+ STRENGTH_CEILING = 0.95
15
+
16
+ EVIDENCE_THRESHOLD = 3
17
+ CAUSAL_THRESHOLD = 0.6
18
+
19
+ REINFORCEMENT_RATE = 0.1
20
+ DECAY_RATE = 0.01
21
+
22
+ EDGE_TYPES = %i[causes prevents enables inhibits modulates].freeze
23
+ INFERENCE_TYPES = %i[observation intervention counterfactual].freeze
24
+
25
+ CONFIDENCE_LABELS = {
26
+ (0.8..) => :strong,
27
+ (0.6...0.8) => :moderate,
28
+ (0.4...0.6) => :weak,
29
+ (0.2...0.4) => :tentative,
30
+ (..0.2) => :speculative
31
+ }.freeze
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CausalReasoning
6
+ module Runners
7
+ module CausalReasoning
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def add_causal_variable(name:, domain: :general, **)
12
+ if graph.variable_exists?(name)
13
+ Legion::Logging.warn "[causal] add_variable duplicate: name=#{name}"
14
+ return { success: false, reason: :limit_or_duplicate, name: name }
15
+ end
16
+
17
+ variable = graph.add_variable(name: name, domain: domain)
18
+ if variable
19
+ Legion::Logging.debug "[causal] add_variable: name=#{name} domain=#{domain}"
20
+ { success: true, variable: variable }
21
+ else
22
+ Legion::Logging.warn "[causal] add_variable failed (limit): name=#{name}"
23
+ { success: false, reason: :limit_or_duplicate, name: name }
24
+ end
25
+ end
26
+
27
+ def add_causal_edge(cause:, effect:, edge_type:, domain: :general,
28
+ strength: Helpers::Constants::DEFAULT_STRENGTH, **)
29
+ edge = graph.add_edge(cause: cause, effect: effect,
30
+ edge_type: edge_type, domain: domain, strength: strength)
31
+ if edge
32
+ str = edge.strength.round(2)
33
+ Legion::Logging.debug "[causal] add_edge: #{cause}->#{effect} type=#{edge_type} str=#{str}"
34
+ { success: true, edge: edge.to_h }
35
+ else
36
+ Legion::Logging.warn "[causal] add_edge failed: cause=#{cause} effect=#{effect} type=#{edge_type}"
37
+ { success: false, reason: :limit_or_invalid_type, cause: cause, effect: effect }
38
+ end
39
+ end
40
+
41
+ def find_causes(variable:, **)
42
+ edges = graph.causes_of(variable: variable)
43
+ Legion::Logging.debug "[causal] find_causes: variable=#{variable} count=#{edges.size}"
44
+ { success: true, variable: variable, causes: edges.map(&:to_h), count: edges.size }
45
+ end
46
+
47
+ def find_effects(variable:, **)
48
+ edges = graph.effects_of(variable: variable)
49
+ Legion::Logging.debug "[causal] find_effects: variable=#{variable} count=#{edges.size}"
50
+ { success: true, variable: variable, effects: edges.map(&:to_h), count: edges.size }
51
+ end
52
+
53
+ def trace_causal_chain(from:, to:, max_depth: 5, **)
54
+ paths = graph.causal_chain(from: from, to: to, max_depth: max_depth)
55
+ Legion::Logging.debug "[causal] trace_chain: from=#{from} to=#{to} paths=#{paths.size}"
56
+ { success: true, from: from, to: to, paths: paths, path_count: paths.size }
57
+ end
58
+
59
+ def causal_intervention(variable:, value:, **)
60
+ result = graph.intervene(variable: variable, value: value)
61
+ count = result[:downstream_effects].size
62
+ Legion::Logging.info "[causal] intervention: do(#{variable}=#{value}) downstream=#{count}"
63
+ { success: true }.merge(result)
64
+ end
65
+
66
+ def find_confounders(var_a:, var_b:, **)
67
+ common = graph.confounders(var_a: var_a, var_b: var_b)
68
+ Legion::Logging.debug "[causal] confounders: #{var_a} <-> #{var_b} count=#{common.size}"
69
+ { success: true, var_a: var_a, var_b: var_b, confounders: common, count: common.size }
70
+ end
71
+
72
+ def add_causal_evidence(edge_id:, **)
73
+ edge = graph.add_evidence(edge_id: edge_id)
74
+ if edge
75
+ cnt = edge.evidence_count
76
+ str = edge.strength.round(2)
77
+ Legion::Logging.debug "[causal] add_evidence: edge=#{edge_id} count=#{cnt} strength=#{str}"
78
+ { success: true, edge_id: edge_id, evidence_count: edge.evidence_count, strength: edge.strength }
79
+ else
80
+ Legion::Logging.warn "[causal] add_evidence failed: edge=#{edge_id} not found"
81
+ { success: false, reason: :edge_not_found, edge_id: edge_id }
82
+ end
83
+ end
84
+
85
+ def update_causal_reasoning(**)
86
+ decayed = graph.decay_all
87
+ pruned = graph.prune_weak
88
+ Legion::Logging.debug "[causal] update: decayed=#{decayed} pruned=#{pruned}"
89
+ { success: true, decayed: decayed, pruned: pruned }
90
+ end
91
+
92
+ def causal_reasoning_stats(**)
93
+ stats = graph.to_h
94
+ Legion::Logging.debug "[causal] stats: #{stats.inspect}"
95
+ { success: true }.merge(stats)
96
+ end
97
+
98
+ private
99
+
100
+ def graph
101
+ @graph ||= Helpers::CausalGraph.new
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CausalReasoning
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/causal_reasoning/version'
4
+ require 'legion/extensions/causal_reasoning/helpers/constants'
5
+ require 'legion/extensions/causal_reasoning/helpers/causal_edge'
6
+ require 'legion/extensions/causal_reasoning/helpers/causal_graph'
7
+ require 'legion/extensions/causal_reasoning/runners/causal_reasoning'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CausalReasoning
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/causal_reasoning/client'
4
+
5
+ RSpec.describe Legion::Extensions::CausalReasoning::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:add_causal_variable)
10
+ expect(client).to respond_to(:add_causal_edge)
11
+ expect(client).to respond_to(:find_causes)
12
+ expect(client).to respond_to(:find_effects)
13
+ expect(client).to respond_to(:trace_causal_chain)
14
+ expect(client).to respond_to(:causal_intervention)
15
+ expect(client).to respond_to(:find_confounders)
16
+ expect(client).to respond_to(:add_causal_evidence)
17
+ expect(client).to respond_to(:update_causal_reasoning)
18
+ expect(client).to respond_to(:causal_reasoning_stats)
19
+ end
20
+
21
+ it 'accepts an injected graph' do
22
+ injected_graph = Legion::Extensions::CausalReasoning::Helpers::CausalGraph.new
23
+ injected_graph.add_edge(cause: :a, effect: :b, edge_type: :causes)
24
+ client_with_graph = described_class.new(graph: injected_graph)
25
+ result = client_with_graph.causal_reasoning_stats
26
+ expect(result[:edges]).to eq(1)
27
+ end
28
+
29
+ it 'maintains isolated state between instances' do
30
+ c1 = described_class.new
31
+ c2 = described_class.new
32
+ c1.add_causal_edge(cause: :a, effect: :b, edge_type: :causes)
33
+ expect(c2.causal_reasoning_stats[:edges]).to eq(0)
34
+ end
35
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/causal_reasoning/client'
4
+
5
+ RSpec.describe Legion::Extensions::CausalReasoning::Helpers::CausalEdge do
6
+ subject(:edge) do
7
+ described_class.new(cause: :rain, effect: :wet_grass, edge_type: :causes, domain: :weather)
8
+ end
9
+
10
+ describe '#initialize' do
11
+ it 'sets cause and effect' do
12
+ expect(edge.cause).to eq(:rain)
13
+ expect(edge.effect).to eq(:wet_grass)
14
+ end
15
+
16
+ it 'sets edge_type and domain' do
17
+ expect(edge.edge_type).to eq(:causes)
18
+ expect(edge.domain).to eq(:weather)
19
+ end
20
+
21
+ it 'clamps strength to STRENGTH_FLOOR..STRENGTH_CEILING' do
22
+ over = described_class.new(cause: :a, effect: :b, edge_type: :causes, strength: 2.0)
23
+ under = described_class.new(cause: :a, effect: :b, edge_type: :causes, strength: 0.0)
24
+ expect(over.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_CEILING)
25
+ expect(under.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_FLOOR)
26
+ end
27
+
28
+ it 'starts with evidence_count of zero' do
29
+ expect(edge.evidence_count).to eq(0)
30
+ end
31
+
32
+ it 'assigns a unique id' do
33
+ other = described_class.new(cause: :rain, effect: :wet_grass, edge_type: :causes)
34
+ expect(edge.id).not_to eq(other.id)
35
+ end
36
+ end
37
+
38
+ describe '#add_evidence' do
39
+ it 'increments evidence_count' do
40
+ edge.add_evidence
41
+ expect(edge.evidence_count).to eq(1)
42
+ end
43
+
44
+ it 'increases strength' do
45
+ before = edge.strength
46
+ edge.add_evidence
47
+ expect(edge.strength).to be > before
48
+ end
49
+
50
+ it 'returns self' do
51
+ expect(edge.add_evidence).to be(edge)
52
+ end
53
+
54
+ it 'does not exceed STRENGTH_CEILING' do
55
+ 100.times { edge.add_evidence }
56
+ expect(edge.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_CEILING)
57
+ end
58
+ end
59
+
60
+ describe '#remove_evidence' do
61
+ it 'decrements evidence_count but not below zero' do
62
+ edge.remove_evidence
63
+ expect(edge.evidence_count).to eq(0)
64
+ end
65
+
66
+ it 'decreases strength' do
67
+ before = edge.strength
68
+ edge.remove_evidence
69
+ expect(edge.strength).to be < before
70
+ end
71
+
72
+ it 'does not go below STRENGTH_FLOOR' do
73
+ 100.times { edge.remove_evidence }
74
+ expect(edge.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_FLOOR)
75
+ end
76
+ end
77
+
78
+ describe '#reinforce' do
79
+ it 'increases strength by given amount' do
80
+ before = edge.strength
81
+ edge.reinforce(amount: 0.2)
82
+ expect(edge.strength).to be_within(0.001).of(before + 0.2)
83
+ end
84
+
85
+ it 'clamps at STRENGTH_CEILING' do
86
+ edge.reinforce(amount: 1.0)
87
+ expect(edge.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_CEILING)
88
+ end
89
+ end
90
+
91
+ describe '#weaken' do
92
+ it 'decreases strength by given amount' do
93
+ before = edge.strength
94
+ edge.weaken(amount: 0.1)
95
+ expect(edge.strength).to be_within(0.001).of(before - 0.1)
96
+ end
97
+
98
+ it 'clamps at STRENGTH_FLOOR' do
99
+ edge.weaken(amount: 1.0)
100
+ expect(edge.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_FLOOR)
101
+ end
102
+ end
103
+
104
+ describe '#decay' do
105
+ it 'reduces strength by DECAY_RATE' do
106
+ before = edge.strength
107
+ edge.decay
108
+ expect(edge.strength).to be_within(0.001).of(before - Legion::Extensions::CausalReasoning::Helpers::Constants::DECAY_RATE)
109
+ end
110
+
111
+ it 'does not go below STRENGTH_FLOOR' do
112
+ 100.times { edge.decay }
113
+ expect(edge.strength).to eq(Legion::Extensions::CausalReasoning::Helpers::Constants::STRENGTH_FLOOR)
114
+ end
115
+ end
116
+
117
+ describe '#confident?' do
118
+ it 'returns false when evidence_count is below EVIDENCE_THRESHOLD' do
119
+ expect(edge.confident?).to be false
120
+ end
121
+
122
+ it 'returns true when both strength and evidence_count meet thresholds' do
123
+ 3.times { edge.add_evidence }
124
+ edge.reinforce(amount: 0.2)
125
+ expect(edge.confident?).to be true
126
+ end
127
+ end
128
+
129
+ describe '#confidence_label' do
130
+ it 'returns :speculative for low strength' do
131
+ edge.weaken(amount: 1.0)
132
+ expect(edge.confidence_label).to eq(:speculative)
133
+ end
134
+
135
+ it 'returns :moderate for strength around 0.7' do
136
+ strong_edge = described_class.new(cause: :a, effect: :b, edge_type: :causes, strength: 0.7)
137
+ expect(strong_edge.confidence_label).to eq(:moderate)
138
+ end
139
+
140
+ it 'returns :strong for strength >= 0.8' do
141
+ strong_edge = described_class.new(cause: :a, effect: :b, edge_type: :causes, strength: 0.9)
142
+ expect(strong_edge.confidence_label).to eq(:strong)
143
+ end
144
+ end
145
+
146
+ describe '#to_h' do
147
+ it 'includes all key fields' do
148
+ hash = edge.to_h
149
+ expect(hash).to include(:id, :cause, :effect, :edge_type, :domain,
150
+ :strength, :evidence_count, :confident, :label,
151
+ :created_at, :updated_at)
152
+ end
153
+
154
+ it 'reflects confident? state' do
155
+ expect(edge.to_h[:confident]).to be false
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/causal_reasoning/client'
4
+
5
+ RSpec.describe Legion::Extensions::CausalReasoning::Helpers::CausalGraph do
6
+ subject(:graph) { described_class.new }
7
+
8
+ let(:const) { Legion::Extensions::CausalReasoning::Helpers::Constants }
9
+
10
+ def add_rain_wet(grph = graph)
11
+ grph.add_edge(cause: :rain, effect: :wet_grass, edge_type: :causes, domain: :weather)
12
+ end
13
+
14
+ describe '#add_variable' do
15
+ it 'registers a new variable and returns it' do
16
+ var = graph.add_variable(name: :temperature, domain: :weather)
17
+ expect(var[:name]).to eq(:temperature)
18
+ expect(var[:domain]).to eq(:weather)
19
+ end
20
+
21
+ it 'returns nil for a duplicate variable name' do
22
+ graph.add_variable(name: :temperature, domain: :weather)
23
+ result = graph.add_variable(name: :temperature, domain: :weather)
24
+ expect(result).to be_nil
25
+ end
26
+ end
27
+
28
+ describe '#add_edge' do
29
+ it 'creates and returns a CausalEdge' do
30
+ edge = add_rain_wet
31
+ expect(edge).to be_a(Legion::Extensions::CausalReasoning::Helpers::CausalEdge)
32
+ end
33
+
34
+ it 'auto-registers both cause and effect as variables' do
35
+ add_rain_wet
36
+ expect(graph.to_h[:variables]).to eq(2)
37
+ end
38
+
39
+ it 'returns nil for unknown edge_type' do
40
+ result = graph.add_edge(cause: :a, effect: :b, edge_type: :unknown_type)
41
+ expect(result).to be_nil
42
+ end
43
+
44
+ it 'accepts all valid EDGE_TYPES' do
45
+ const::EDGE_TYPES.each_with_index do |type, idx|
46
+ result = graph.add_edge(cause: :"cause_#{idx}", effect: :"effect_#{idx}", edge_type: type)
47
+ expect(result).not_to be_nil
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#remove_edge' do
53
+ it 'removes an existing edge and returns it' do
54
+ edge = add_rain_wet
55
+ removed = graph.remove_edge(edge_id: edge.id)
56
+ expect(removed.id).to eq(edge.id)
57
+ expect(graph.to_h[:edges]).to eq(0)
58
+ end
59
+
60
+ it 'returns nil for unknown edge_id' do
61
+ expect(graph.remove_edge(edge_id: 'nonexistent')).to be_nil
62
+ end
63
+ end
64
+
65
+ describe '#causes_of' do
66
+ it 'returns edges where the variable is the effect' do
67
+ add_rain_wet
68
+ causes = graph.causes_of(variable: :wet_grass)
69
+ expect(causes.size).to eq(1)
70
+ expect(causes.first.cause).to eq(:rain)
71
+ end
72
+
73
+ it 'returns empty array for variable with no causes' do
74
+ expect(graph.causes_of(variable: :rain)).to be_empty
75
+ end
76
+ end
77
+
78
+ describe '#effects_of' do
79
+ it 'returns edges where the variable is the cause' do
80
+ add_rain_wet
81
+ effects = graph.effects_of(variable: :rain)
82
+ expect(effects.size).to eq(1)
83
+ expect(effects.first.effect).to eq(:wet_grass)
84
+ end
85
+
86
+ it 'returns empty array for unknown variable' do
87
+ expect(graph.effects_of(variable: :unknown)).to be_empty
88
+ end
89
+ end
90
+
91
+ describe '#causal_chain' do
92
+ before do
93
+ graph.add_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
94
+ graph.add_edge(cause: :wet_grass, effect: :slippery_path, edge_type: :causes)
95
+ end
96
+
97
+ it 'finds a direct path between connected variables' do
98
+ paths = graph.causal_chain(from: :rain, to: :slippery_path)
99
+ expect(paths).not_to be_empty
100
+ expect(paths.first).to eq(%i[rain wet_grass slippery_path])
101
+ end
102
+
103
+ it 'returns empty array when no path exists' do
104
+ paths = graph.causal_chain(from: :slippery_path, to: :rain)
105
+ expect(paths).to be_empty
106
+ end
107
+
108
+ it 'returns empty array when from == to' do
109
+ paths = graph.causal_chain(from: :rain, to: :rain)
110
+ expect(paths).to be_empty
111
+ end
112
+
113
+ it 'respects max_depth' do
114
+ graph.add_edge(cause: :slippery_path, effect: :injury, edge_type: :causes)
115
+ graph.add_edge(cause: :injury, effect: :hospital_visit, edge_type: :causes)
116
+ paths = graph.causal_chain(from: :rain, to: :hospital_visit, max_depth: 2)
117
+ expect(paths).to be_empty
118
+ end
119
+ end
120
+
121
+ describe '#intervene' do
122
+ before do
123
+ graph.add_edge(cause: :smoking, effect: :cancer, edge_type: :causes)
124
+ graph.add_edge(cause: :cancer, effect: :death, edge_type: :causes)
125
+ end
126
+
127
+ it 'returns the intervention variable and value' do
128
+ result = graph.intervene(variable: :smoking, value: true)
129
+ expect(result[:intervention]).to eq(:smoking)
130
+ expect(result[:value]).to be true
131
+ end
132
+
133
+ it 'lists all downstream effects' do
134
+ result = graph.intervene(variable: :smoking, value: true)
135
+ variables = result[:downstream_effects].map { |e| e[:variable] }
136
+ expect(variables).to include(:cancer, :death)
137
+ end
138
+
139
+ it 'returns empty downstream for leaf variable' do
140
+ result = graph.intervene(variable: :death, value: true)
141
+ expect(result[:downstream_effects]).to be_empty
142
+ end
143
+ end
144
+
145
+ describe '#observe' do
146
+ it 'updates edges involving the variable when evidence is true' do
147
+ edge = add_rain_wet
148
+ before_count = edge.evidence_count
149
+ graph.observe(variable: :rain, value: true, evidence: true)
150
+ expect(edge.evidence_count).to be > before_count
151
+ end
152
+
153
+ it 'returns the variable, value, and count of edges updated' do
154
+ add_rain_wet
155
+ result = graph.observe(variable: :rain, value: true, evidence: true)
156
+ expect(result[:variable]).to eq(:rain)
157
+ expect(result[:edges_updated]).to eq(1)
158
+ end
159
+ end
160
+
161
+ describe '#confounders' do
162
+ it 'finds common ancestors of two variables' do
163
+ graph.add_edge(cause: :weather, effect: :rain, edge_type: :causes)
164
+ graph.add_edge(cause: :weather, effect: :cold, edge_type: :causes)
165
+ common = graph.confounders(var_a: :rain, var_b: :cold)
166
+ expect(common).to include(:weather)
167
+ end
168
+
169
+ it 'returns empty array when no confounders exist' do
170
+ graph.add_edge(cause: :a, effect: :b, edge_type: :causes)
171
+ graph.add_edge(cause: :c, effect: :d, edge_type: :causes)
172
+ expect(graph.confounders(var_a: :b, var_b: :d)).to be_empty
173
+ end
174
+ end
175
+
176
+ describe '#add_evidence and #remove_evidence' do
177
+ it 'delegates add_evidence to the edge' do
178
+ edge = add_rain_wet
179
+ graph.add_evidence(edge_id: edge.id)
180
+ expect(edge.evidence_count).to eq(1)
181
+ end
182
+
183
+ it 'returns nil for unknown edge_id in add_evidence' do
184
+ expect(graph.add_evidence(edge_id: 'bad_id')).to be_nil
185
+ end
186
+
187
+ it 'delegates remove_evidence to the edge' do
188
+ edge = add_rain_wet
189
+ edge.add_evidence
190
+ graph.remove_evidence(edge_id: edge.id)
191
+ expect(edge.evidence_count).to eq(0)
192
+ end
193
+ end
194
+
195
+ describe '#confident_edges' do
196
+ it 'returns only edges meeting confidence threshold' do
197
+ edge = add_rain_wet
198
+ 3.times { edge.add_evidence }
199
+ edge.reinforce(amount: 0.2)
200
+ expect(graph.confident_edges).to include(edge)
201
+ end
202
+
203
+ it 'excludes edges below threshold' do
204
+ add_rain_wet
205
+ expect(graph.confident_edges).to be_empty
206
+ end
207
+ end
208
+
209
+ describe '#by_domain and #by_type' do
210
+ before do
211
+ graph.add_edge(cause: :rain, effect: :wet_grass, edge_type: :causes, domain: :weather)
212
+ graph.add_edge(cause: :stress, effect: :illness, edge_type: :causes, domain: :health)
213
+ end
214
+
215
+ it 'filters edges by domain' do
216
+ expect(graph.by_domain(domain: :weather).size).to eq(1)
217
+ expect(graph.by_domain(domain: :health).size).to eq(1)
218
+ end
219
+
220
+ it 'filters edges by type' do
221
+ expect(graph.by_type(type: :causes).size).to eq(2)
222
+ expect(graph.by_type(type: :prevents).size).to eq(0)
223
+ end
224
+ end
225
+
226
+ describe '#decay_all' do
227
+ it 'decays all edges and returns total count' do
228
+ add_rain_wet
229
+ result = graph.decay_all
230
+ expect(result).to eq(1)
231
+ end
232
+ end
233
+
234
+ describe '#prune_weak' do
235
+ it 'removes edges at STRENGTH_FLOOR and returns count' do
236
+ edge = add_rain_wet
237
+ 100.times { edge.weaken }
238
+ pruned = graph.prune_weak
239
+ expect(pruned).to eq(1)
240
+ expect(graph.to_h[:edges]).to eq(0)
241
+ end
242
+
243
+ it 'returns 0 when no weak edges exist' do
244
+ add_rain_wet
245
+ expect(graph.prune_weak).to eq(0)
246
+ end
247
+ end
248
+
249
+ describe '#to_h' do
250
+ it 'returns stats hash with variables, edges, confident_edges, edge_types' do
251
+ add_rain_wet
252
+ stats = graph.to_h
253
+ expect(stats[:variables]).to eq(2)
254
+ expect(stats[:edges]).to eq(1)
255
+ expect(stats[:edge_types]).to be_a(Hash)
256
+ expect(stats[:edge_types][:causes]).to eq(1)
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/causal_reasoning/client'
4
+
5
+ RSpec.describe Legion::Extensions::CausalReasoning::Runners::CausalReasoning do
6
+ let(:client) { Legion::Extensions::CausalReasoning::Client.new }
7
+
8
+ describe '#add_causal_variable' do
9
+ it 'returns success: true and the variable hash' do
10
+ result = client.add_causal_variable(name: :temperature, domain: :weather)
11
+ expect(result[:success]).to be true
12
+ expect(result[:variable][:name]).to eq(:temperature)
13
+ end
14
+
15
+ it 'returns success: false for duplicate variable' do
16
+ client.add_causal_variable(name: :temperature, domain: :weather)
17
+ result = client.add_causal_variable(name: :temperature, domain: :weather)
18
+ expect(result[:success]).to be false
19
+ expect(result[:reason]).to eq(:limit_or_duplicate)
20
+ end
21
+ end
22
+
23
+ describe '#add_causal_edge' do
24
+ it 'returns success: true and an edge hash' do
25
+ result = client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
26
+ expect(result[:success]).to be true
27
+ expect(result[:edge][:cause]).to eq(:rain)
28
+ expect(result[:edge][:effect]).to eq(:wet_grass)
29
+ end
30
+
31
+ it 'returns success: false for invalid edge_type' do
32
+ result = client.add_causal_edge(cause: :a, effect: :b, edge_type: :unknown)
33
+ expect(result[:success]).to be false
34
+ expect(result[:reason]).to eq(:limit_or_invalid_type)
35
+ end
36
+
37
+ it 'uses given strength' do
38
+ result = client.add_causal_edge(cause: :a, effect: :b, edge_type: :causes, strength: 0.8)
39
+ expect(result[:edge][:strength]).to be_within(0.001).of(0.8)
40
+ end
41
+ end
42
+
43
+ describe '#find_causes' do
44
+ it 'returns causes of a variable' do
45
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
46
+ result = client.find_causes(variable: :wet_grass)
47
+ expect(result[:success]).to be true
48
+ expect(result[:count]).to eq(1)
49
+ expect(result[:causes].first[:cause]).to eq(:rain)
50
+ end
51
+
52
+ it 'returns empty causes for variable with no incoming edges' do
53
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
54
+ result = client.find_causes(variable: :rain)
55
+ expect(result[:count]).to eq(0)
56
+ end
57
+ end
58
+
59
+ describe '#find_effects' do
60
+ it 'returns effects of a variable' do
61
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
62
+ result = client.find_effects(variable: :rain)
63
+ expect(result[:success]).to be true
64
+ expect(result[:count]).to eq(1)
65
+ expect(result[:effects].first[:effect]).to eq(:wet_grass)
66
+ end
67
+
68
+ it 'returns empty effects for leaf variable' do
69
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
70
+ result = client.find_effects(variable: :wet_grass)
71
+ expect(result[:count]).to eq(0)
72
+ end
73
+ end
74
+
75
+ describe '#trace_causal_chain' do
76
+ before do
77
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
78
+ client.add_causal_edge(cause: :wet_grass, effect: :slippery, edge_type: :causes)
79
+ end
80
+
81
+ it 'finds a multi-hop causal path' do
82
+ result = client.trace_causal_chain(from: :rain, to: :slippery)
83
+ expect(result[:success]).to be true
84
+ expect(result[:path_count]).to be >= 1
85
+ end
86
+
87
+ it 'returns zero paths when no connection' do
88
+ result = client.trace_causal_chain(from: :slippery, to: :rain)
89
+ expect(result[:path_count]).to eq(0)
90
+ end
91
+ end
92
+
93
+ describe '#causal_intervention' do
94
+ before do
95
+ client.add_causal_edge(cause: :smoking, effect: :cancer, edge_type: :causes)
96
+ client.add_causal_edge(cause: :cancer, effect: :treatment, edge_type: :causes)
97
+ end
98
+
99
+ it 'returns success: true and lists downstream effects' do
100
+ result = client.causal_intervention(variable: :smoking, value: true)
101
+ expect(result[:success]).to be true
102
+ expect(result[:intervention]).to eq(:smoking)
103
+ downstream_vars = result[:downstream_effects].map { |e| e[:variable] }
104
+ expect(downstream_vars).to include(:cancer, :treatment)
105
+ end
106
+ end
107
+
108
+ describe '#find_confounders' do
109
+ before do
110
+ client.add_causal_edge(cause: :weather, effect: :rain, edge_type: :causes)
111
+ client.add_causal_edge(cause: :weather, effect: :cold, edge_type: :causes)
112
+ end
113
+
114
+ it 'identifies common ancestors as confounders' do
115
+ result = client.find_confounders(var_a: :rain, var_b: :cold)
116
+ expect(result[:success]).to be true
117
+ expect(result[:confounders]).to include(:weather)
118
+ end
119
+
120
+ it 'returns empty confounders for unrelated variables' do
121
+ result = client.find_confounders(var_a: :rain, var_b: :unrelated)
122
+ expect(result[:count]).to eq(0)
123
+ end
124
+ end
125
+
126
+ describe '#add_causal_evidence' do
127
+ it 'increments evidence on an existing edge' do
128
+ edge_result = client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
129
+ edge_id = edge_result[:edge][:id]
130
+ result = client.add_causal_evidence(edge_id: edge_id)
131
+ expect(result[:success]).to be true
132
+ expect(result[:evidence_count]).to eq(1)
133
+ end
134
+
135
+ it 'returns success: false for unknown edge_id' do
136
+ result = client.add_causal_evidence(edge_id: 'nonexistent-uuid')
137
+ expect(result[:success]).to be false
138
+ expect(result[:reason]).to eq(:edge_not_found)
139
+ end
140
+ end
141
+
142
+ describe '#update_causal_reasoning' do
143
+ it 'returns success: true with decayed and pruned counts' do
144
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
145
+ result = client.update_causal_reasoning
146
+ expect(result[:success]).to be true
147
+ expect(result[:decayed]).to eq(1)
148
+ expect(result[:pruned]).to eq(0)
149
+ end
150
+ end
151
+
152
+ describe '#causal_reasoning_stats' do
153
+ it 'returns stats including variables and edges counts' do
154
+ client.add_causal_edge(cause: :rain, effect: :wet_grass, edge_type: :causes)
155
+ result = client.causal_reasoning_stats
156
+ expect(result[:success]).to be true
157
+ expect(result[:variables]).to eq(2)
158
+ expect(result[:edges]).to eq(1)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,24 @@
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
+
13
+ module Extensions
14
+ module Helpers; end
15
+ end
16
+ end
17
+
18
+ require 'legion/extensions/causal_reasoning'
19
+
20
+ RSpec.configure do |config|
21
+ config.example_status_persistence_file_path = '.rspec_status'
22
+ config.disable_monkey_patching!
23
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
24
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-causal-reasoning
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: Causal inference engine for brain-modeled agentic AI — do-calculus and
27
+ causal graphs
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-causal-reasoning.gemspec
36
+ - lib/legion/extensions/causal_reasoning.rb
37
+ - lib/legion/extensions/causal_reasoning/client.rb
38
+ - lib/legion/extensions/causal_reasoning/helpers/causal_edge.rb
39
+ - lib/legion/extensions/causal_reasoning/helpers/causal_graph.rb
40
+ - lib/legion/extensions/causal_reasoning/helpers/constants.rb
41
+ - lib/legion/extensions/causal_reasoning/runners/causal_reasoning.rb
42
+ - lib/legion/extensions/causal_reasoning/version.rb
43
+ - spec/legion/extensions/causal_reasoning/client_spec.rb
44
+ - spec/legion/extensions/causal_reasoning/helpers/causal_edge_spec.rb
45
+ - spec/legion/extensions/causal_reasoning/helpers/causal_graph_spec.rb
46
+ - spec/legion/extensions/causal_reasoning/runners/causal_reasoning_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-causal-reasoning
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-causal-reasoning
53
+ source_code_uri: https://github.com/LegionIO/lex-causal-reasoning
54
+ documentation_uri: https://github.com/LegionIO/lex-causal-reasoning
55
+ changelog_uri: https://github.com/LegionIO/lex-causal-reasoning
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-causal-reasoning/issues
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: LEX Causal Reasoning
75
+ test_files: []