lex-imagination 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: 796ec9d97b9cc1782cf232c968efd46f14dcdc7612b4f1122368071729c86bd4
4
+ data.tar.gz: ac83b7c6f4aa92c5017bfed3afd2ef19a193f11054a84f726faffd27d94b159f
5
+ SHA512:
6
+ metadata.gz: 8a0f5965a249e2eee28d411d9d03755bf38c70cc5194ca3ae6c88eeb6d30b7f3b012c63112f11307b4e3b054da20f4fc1aa132d61f92308389641ea8a499e3bf
7
+ data.tar.gz: 79402cb3ff9b14aa04bcd3b15f3ccaa6217c2aacdae953e8c26f9894daa133b2aecd5ef0197b7a907c9218cf891a2143d13fcabbfa836360cd25dc1ee1512ae1
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :dev, :test do
8
+ gem 'rspec', '~> 3.12'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec', '~> 3.0'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Iverson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # lex-imagination
2
+
3
+ Counterfactual simulation and mental rehearsal for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
4
+
5
+ ## What It Does
6
+
7
+ `lex-imagination` lets an agent mentally rehearse actions before committing to them. Given a list of candidate actions, it builds scenarios with success, failure, and wildcard outcomes, evaluates each via a weighted composite score (expected value, risk, reversibility, novelty, alignment), and returns a ranked recommendation. Deep what-if chains trace consequences recursively. Actual outcomes can be recorded to track simulation accuracy over time.
8
+
9
+ Key capabilities:
10
+
11
+ - **Multi-scenario simulation**: evaluate up to 5 candidate actions per call
12
+ - **Composite scoring**: expected value (35%), risk (25%), reversibility (20%), novelty (10%), alignment (10%)
13
+ - **Recursive what-if chains**: trace consequences up to depth 3
14
+ - **Head-to-head comparison**: compare two actions directly
15
+ - **Outcome tracking**: record actual results to measure simulation accuracy
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'lex-imagination'
23
+ ```
24
+
25
+ Or install directly:
26
+
27
+ ```
28
+ gem install lex-imagination
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ require 'legion/extensions/imagination'
35
+
36
+ client = Legion::Extensions::Imagination::Client.new
37
+
38
+ # Simulate candidate actions
39
+ result = client.simulate(
40
+ actions: ['deploy_hotfix', 'wait_for_review', 'rollback'],
41
+ context: { familiar: true, alignment: 0.8, risky: false },
42
+ risk_tolerance: :moderate
43
+ )
44
+ # => { recommendation: { action: 'deploy_hotfix', composite: 0.72, confidence: :high },
45
+ # scenarios: [...], simulation_id: "..." }
46
+
47
+ # Deep what-if analysis
48
+ chain = client.what_if(action: 'merge_without_tests', context: { risky: true }, depth: 3)
49
+ # => { consequence_chain: [...], overall_valence: :negative_trajectory }
50
+
51
+ # Compare two options
52
+ comparison = client.compare(action_a: 'refactor_first', action_b: 'ship_now', context: {})
53
+ # => { winner: :a, margin: 0.15, decisive: true }
54
+
55
+ # Record what actually happened
56
+ client.record_actual_outcome(simulation_id: result[:simulation_id], actual_outcome: :success)
57
+
58
+ # Stats
59
+ client.imagination_stats
60
+ ```
61
+
62
+ ## Runner Methods
63
+
64
+ | Method | Description |
65
+ |---|---|
66
+ | `simulate` | Build and rank scenarios for a list of candidate actions |
67
+ | `what_if` | Recursive consequence chain for a single action |
68
+ | `compare` | Head-to-head composite score comparison of two actions |
69
+ | `record_actual_outcome` | Log actual result for accuracy tracking |
70
+ | `imagination_stats` | Total simulations, accuracy, avg composite |
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ bundle install
76
+ bundle exec rspec
77
+ bundle exec rubocop
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/imagination/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-imagination'
7
+ spec.version = Legion::Extensions::Imagination::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Imagination'
12
+ spec.description = 'Counterfactual simulation engine — mental rehearsal of actions before committing'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-imagination'
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-imagination'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-imagination'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-imagination'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-imagination/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-imagination.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ class Client
7
+ include Runners::Imagination
8
+
9
+ attr_reader :simulation_store
10
+
11
+ def initialize(simulation_store: nil, **)
12
+ @simulation_store = simulation_store || Helpers::SimulationStore.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ module Helpers
7
+ module Constants
8
+ # Maximum scenarios per simulation
9
+ MAX_SCENARIOS = 5
10
+
11
+ # Maximum simulation depth (chained consequence steps)
12
+ MAX_DEPTH = 3
13
+
14
+ # Simulation history capacity
15
+ MAX_SIMULATIONS = 100
16
+
17
+ # Confidence thresholds
18
+ HIGH_CONFIDENCE = 0.7
19
+ MEDIUM_CONFIDENCE = 0.4
20
+ LOW_CONFIDENCE = 0.2
21
+
22
+ # Outcome valence labels
23
+ OUTCOME_VALENCES = %i[very_positive positive neutral negative very_negative].freeze
24
+
25
+ # Risk tolerance levels
26
+ RISK_TOLERANCES = %i[conservative moderate aggressive].freeze
27
+
28
+ # Default evaluation weights
29
+ EVALUATION_WEIGHTS = {
30
+ expected_value: 0.3,
31
+ risk: 0.25,
32
+ reversibility: 0.2,
33
+ alignment: 0.15,
34
+ novelty: 0.1
35
+ }.freeze
36
+
37
+ # Scenario outcome types
38
+ OUTCOME_TYPES = %i[success partial_success neutral partial_failure failure wildcard].freeze
39
+
40
+ # Imagination modes
41
+ MODES = %i[
42
+ prospective
43
+ retrospective
44
+ counterfactual
45
+ exploratory
46
+ ].freeze
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ module Helpers
7
+ class Scenario
8
+ attr_reader :scenario_id, :action, :context, :outcomes, :evaluation, :created_at
9
+
10
+ def initialize(action:, context: {})
11
+ @scenario_id = SecureRandom.uuid
12
+ @action = action
13
+ @context = context
14
+ @outcomes = []
15
+ @evaluation = nil
16
+ @created_at = Time.now.utc
17
+ end
18
+
19
+ def add_outcome(outcome)
20
+ @outcomes << {
21
+ outcome_id: SecureRandom.uuid,
22
+ type: outcome[:type] || :neutral,
23
+ likelihood: (outcome[:likelihood] || 0.5).clamp(0.0, 1.0),
24
+ valence: classify_valence(outcome[:value] || 0.0),
25
+ value: (outcome[:value] || 0.0).clamp(-1.0, 1.0),
26
+ description: outcome[:description],
27
+ consequences: outcome[:consequences] || [],
28
+ reversible: outcome.fetch(:reversible, true)
29
+ }
30
+ end
31
+
32
+ def expected_value
33
+ return 0.0 if @outcomes.empty?
34
+
35
+ @outcomes.sum { |o| o[:likelihood] * o[:value] }
36
+ end
37
+
38
+ def risk_score
39
+ return 0.0 if @outcomes.empty?
40
+
41
+ negative_outcomes = @outcomes.select { |o| o[:value].negative? }
42
+ return 0.0 if negative_outcomes.empty?
43
+
44
+ negative_outcomes.sum { |o| o[:likelihood] * o[:value].abs }
45
+ end
46
+
47
+ def best_outcome
48
+ @outcomes.max_by { |o| o[:value] }
49
+ end
50
+
51
+ def worst_outcome
52
+ @outcomes.min_by { |o| o[:value] }
53
+ end
54
+
55
+ def reversibility
56
+ return 1.0 if @outcomes.empty?
57
+
58
+ reversible_count = @outcomes.count { |o| o[:reversible] }
59
+ reversible_count.to_f / @outcomes.size
60
+ end
61
+
62
+ def evaluate(weights: Constants::EVALUATION_WEIGHTS, alignment: 0.5, novelty: 0.5)
63
+ @evaluation = {
64
+ expected_value: expected_value,
65
+ risk: risk_score,
66
+ reversibility: reversibility,
67
+ alignment: alignment,
68
+ novelty: novelty,
69
+ composite: compute_composite(weights, alignment, novelty),
70
+ evaluated_at: Time.now.utc
71
+ }
72
+ end
73
+
74
+ def to_h
75
+ {
76
+ scenario_id: @scenario_id,
77
+ action: @action,
78
+ context: @context,
79
+ outcomes: @outcomes,
80
+ expected_value: expected_value,
81
+ risk_score: risk_score,
82
+ reversibility: reversibility,
83
+ evaluation: @evaluation,
84
+ created_at: @created_at
85
+ }
86
+ end
87
+
88
+ private
89
+
90
+ def classify_valence(value)
91
+ if value > 0.5
92
+ :very_positive
93
+ elsif value > 0.1
94
+ :positive
95
+ elsif value > -0.1
96
+ :neutral
97
+ elsif value > -0.5
98
+ :negative
99
+ else
100
+ :very_negative
101
+ end
102
+ end
103
+
104
+ def compute_composite(weights, alignment, novelty)
105
+ ev_score = (expected_value + 1.0) / 2.0
106
+ risk_penalty = 1.0 - risk_score
107
+ rev_score = reversibility
108
+
109
+ (weights[:expected_value] * ev_score) +
110
+ (weights[:risk] * risk_penalty) +
111
+ (weights[:reversibility] * rev_score) +
112
+ (weights[:alignment] * alignment) +
113
+ (weights[:novelty] * novelty)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ module Helpers
7
+ class SimulationStore
8
+ attr_reader :simulations
9
+
10
+ def initialize
11
+ @simulations = []
12
+ end
13
+
14
+ def store(simulation)
15
+ @simulations << simulation
16
+ @simulations = @simulations.last(Constants::MAX_SIMULATIONS)
17
+ simulation
18
+ end
19
+
20
+ def get(simulation_id)
21
+ @simulations.find { |s| s[:simulation_id] == simulation_id }
22
+ end
23
+
24
+ def recent(limit: 10)
25
+ @simulations.last(limit)
26
+ end
27
+
28
+ def by_mode(mode)
29
+ @simulations.select { |s| s[:mode] == mode }
30
+ end
31
+
32
+ def best_decisions
33
+ @simulations.select { |s| s[:recommendation] }
34
+ .sort_by { |s| -(s.dig(:recommendation, :composite) || 0) }
35
+ end
36
+
37
+ def accuracy_check(simulation_id, actual_outcome:)
38
+ sim = get(simulation_id)
39
+ return nil unless sim
40
+
41
+ sim[:actual_outcome] = actual_outcome
42
+ sim[:accurate] = outcome_matches?(sim, actual_outcome)
43
+ sim[:accurate]
44
+ end
45
+
46
+ def simulation_accuracy
47
+ checked = @simulations.select { |s| s.key?(:accurate) }
48
+ return nil if checked.empty?
49
+
50
+ correct = checked.count { |s| s[:accurate] }
51
+ correct.to_f / checked.size
52
+ end
53
+
54
+ def size
55
+ @simulations.size
56
+ end
57
+
58
+ def clear
59
+ @simulations.clear
60
+ end
61
+
62
+ private
63
+
64
+ def outcome_matches?(simulation, actual)
65
+ recommended = simulation.dig(:recommendation, :action)
66
+ return false unless recommended
67
+
68
+ actual_valence = actual[:valence] || :neutral
69
+ positive_outcomes = %i[very_positive positive]
70
+ positive_outcomes.include?(actual_valence)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ module Runners
7
+ module Imagination
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def simulate(actions:, context: {}, mode: :prospective, risk_tolerance: :moderate, **)
12
+ scenarios = actions.first(Helpers::Constants::MAX_SCENARIOS).map do |action|
13
+ build_scenario(action, context)
14
+ end
15
+
16
+ scenarios.each { |s| s.evaluate(alignment: context[:alignment] || 0.5, novelty: context[:novelty] || 0.5) }
17
+
18
+ ranked = rank_scenarios(scenarios, risk_tolerance)
19
+ recommendation = ranked.first
20
+
21
+ simulation = {
22
+ simulation_id: SecureRandom.uuid,
23
+ mode: mode.to_sym,
24
+ scenarios: scenarios.map(&:to_h),
25
+ recommendation: recommendation ? format_recommendation(recommendation) : nil,
26
+ risk_tolerance: risk_tolerance,
27
+ simulated_at: Time.now.utc
28
+ }
29
+
30
+ simulation_store.store(simulation)
31
+
32
+ Legion::Logging.debug "[imagination] simulated #{scenarios.size} scenarios, " \
33
+ "recommended=#{recommendation&.action || 'none'}"
34
+
35
+ simulation
36
+ end
37
+
38
+ def what_if(action:, context: {}, depth: 1, **)
39
+ actual_depth = [depth, Helpers::Constants::MAX_DEPTH].min
40
+ scenario = build_scenario(action, context)
41
+ chain = build_consequence_chain(scenario, actual_depth)
42
+ scenario.evaluate
43
+
44
+ Legion::Logging.debug "[imagination] what_if: action=#{action} depth=#{actual_depth} " \
45
+ "ev=#{scenario.expected_value.round(2)}"
46
+
47
+ {
48
+ scenario: scenario.to_h,
49
+ consequence_chain: chain,
50
+ depth: actual_depth,
51
+ overall_valence: classify_chain_valence(chain)
52
+ }
53
+ end
54
+
55
+ def compare(action_a:, action_b:, context: {}, **)
56
+ scenario_a = build_scenario(action_a, context)
57
+ scenario_b = build_scenario(action_b, context)
58
+ scenario_a.evaluate
59
+ scenario_b.evaluate
60
+
61
+ winner = scenario_a.evaluation[:composite] >= scenario_b.evaluation[:composite] ? :a : :b
62
+ margin = (scenario_a.evaluation[:composite] - scenario_b.evaluation[:composite]).abs
63
+
64
+ Legion::Logging.debug "[imagination] compare: #{action_a} vs #{action_b} -> winner=#{winner} margin=#{margin.round(3)}"
65
+
66
+ {
67
+ scenario_a: scenario_a.to_h,
68
+ scenario_b: scenario_b.to_h,
69
+ winner: winner,
70
+ margin: margin,
71
+ decisive: margin > 0.1
72
+ }
73
+ end
74
+
75
+ def record_actual_outcome(simulation_id:, actual_outcome: {}, **)
76
+ result = simulation_store.accuracy_check(simulation_id, actual_outcome: actual_outcome)
77
+ if result.nil?
78
+ { error: :not_found }
79
+ else
80
+ Legion::Logging.info "[imagination] outcome recorded: simulation=#{simulation_id} accurate=#{result}"
81
+ { simulation_id: simulation_id, accurate: result, overall_accuracy: simulation_store.simulation_accuracy }
82
+ end
83
+ end
84
+
85
+ def imagination_stats(**)
86
+ {
87
+ total_simulations: simulation_store.size,
88
+ accuracy: simulation_store.simulation_accuracy,
89
+ by_mode: mode_distribution,
90
+ recent_count: simulation_store.recent(limit: 10).size
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ def simulation_store
97
+ @simulation_store ||= Helpers::SimulationStore.new
98
+ end
99
+
100
+ def build_scenario(action, context)
101
+ scenario = Helpers::Scenario.new(action: action, context: context)
102
+
103
+ scenario.add_outcome(type: :success, likelihood: estimate_success_likelihood(action, context),
104
+ value: estimate_positive_value(context), description: "#{action} succeeds",
105
+ reversible: true)
106
+
107
+ scenario.add_outcome(type: :failure, likelihood: estimate_failure_likelihood(action, context),
108
+ value: estimate_negative_value(context), description: "#{action} fails",
109
+ reversible: context.fetch(:reversible, true))
110
+
111
+ if context[:wildcard]
112
+ scenario.add_outcome(type: :wildcard, likelihood: 0.1,
113
+ value: context[:wildcard_value] || 0.0,
114
+ description: context[:wildcard], reversible: true)
115
+ end
116
+
117
+ scenario
118
+ end
119
+
120
+ def estimate_success_likelihood(_action, context)
121
+ base = 0.5
122
+ base += 0.1 if context[:familiar]
123
+ base += 0.1 if context[:alignment].is_a?(Numeric) && context[:alignment] > 0.6
124
+ base -= 0.1 if context[:risky]
125
+ base.clamp(0.1, 0.9)
126
+ end
127
+
128
+ def estimate_failure_likelihood(_action, context)
129
+ 1.0 - estimate_success_likelihood(nil, context)
130
+ end
131
+
132
+ def estimate_positive_value(context)
133
+ (context[:positive_value] || 0.6).clamp(0.0, 1.0)
134
+ end
135
+
136
+ def estimate_negative_value(context)
137
+ -(context[:negative_value] || 0.4).clamp(0.0, 1.0)
138
+ end
139
+
140
+ def rank_scenarios(scenarios, risk_tolerance)
141
+ scenarios.sort_by do |s|
142
+ composite = s.evaluation[:composite]
143
+ risk_adjust = case risk_tolerance.to_sym
144
+ when :conservative then composite - (s.risk_score * 0.3)
145
+ when :aggressive then composite + (s.expected_value * 0.2)
146
+ else composite
147
+ end
148
+ -risk_adjust
149
+ end
150
+ end
151
+
152
+ def format_recommendation(scenario)
153
+ {
154
+ action: scenario.action,
155
+ composite: scenario.evaluation[:composite],
156
+ expected_value: scenario.expected_value,
157
+ risk: scenario.risk_score,
158
+ reversibility: scenario.reversibility,
159
+ confidence: classify_confidence(scenario.evaluation[:composite])
160
+ }
161
+ end
162
+
163
+ def classify_confidence(composite)
164
+ if composite >= Helpers::Constants::HIGH_CONFIDENCE
165
+ :high
166
+ elsif composite >= Helpers::Constants::MEDIUM_CONFIDENCE
167
+ :medium
168
+ else
169
+ :low
170
+ end
171
+ end
172
+
173
+ def build_consequence_chain(scenario, depth)
174
+ chain = [{ depth: 0, expected_value: scenario.expected_value, risk: scenario.risk_score }]
175
+
176
+ depth.times do |d|
177
+ prev = chain.last
178
+ dampened_ev = prev[:expected_value] * 0.7
179
+ dampened_risk = prev[:risk] * 0.8
180
+ chain << { depth: d + 1, expected_value: dampened_ev, risk: dampened_risk }
181
+ end
182
+
183
+ chain
184
+ end
185
+
186
+ def classify_chain_valence(chain)
187
+ total_ev = chain.sum { |c| c[:expected_value] }
188
+ if total_ev > 0.3
189
+ :positive_trajectory
190
+ elsif total_ev < -0.3
191
+ :negative_trajectory
192
+ else
193
+ :uncertain_trajectory
194
+ end
195
+ end
196
+
197
+ def mode_distribution
198
+ dist = Hash.new(0)
199
+ simulation_store.simulations.each { |s| dist[s[:mode]] += 1 }
200
+ dist
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Imagination
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'legion/extensions/imagination/version'
5
+ require 'legion/extensions/imagination/helpers/constants'
6
+ require 'legion/extensions/imagination/helpers/scenario'
7
+ require 'legion/extensions/imagination/helpers/simulation_store'
8
+ require 'legion/extensions/imagination/runners/imagination'
9
+ require 'legion/extensions/imagination/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Imagination
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Imagination::Client do
4
+ it 'creates default simulation store' do
5
+ client = described_class.new
6
+ expect(client.simulation_store).to be_a(Legion::Extensions::Imagination::Helpers::SimulationStore)
7
+ end
8
+
9
+ it 'accepts injected simulation store' do
10
+ store = Legion::Extensions::Imagination::Helpers::SimulationStore.new
11
+ client = described_class.new(simulation_store: store)
12
+ expect(client.simulation_store).to equal(store)
13
+ end
14
+
15
+ it 'includes Imagination runner methods' do
16
+ client = described_class.new
17
+ expect(client).to respond_to(:simulate, :what_if, :compare,
18
+ :record_actual_outcome, :imagination_stats)
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Imagination::Helpers::Constants do
4
+ it 'defines 4 imagination modes' do
5
+ expect(described_class::MODES.size).to eq(4)
6
+ end
7
+
8
+ it 'defines 6 outcome types' do
9
+ expect(described_class::OUTCOME_TYPES.size).to eq(6)
10
+ end
11
+
12
+ it 'defines 3 risk tolerances' do
13
+ expect(described_class::RISK_TOLERANCES).to contain_exactly(:conservative, :moderate, :aggressive)
14
+ end
15
+
16
+ it 'defines evaluation weights summing to 1.0' do
17
+ total = described_class::EVALUATION_WEIGHTS.values.sum
18
+ expect(total).to be_within(0.001).of(1.0)
19
+ end
20
+
21
+ it 'defines ordered confidence thresholds' do
22
+ expect(described_class::LOW_CONFIDENCE).to be < described_class::MEDIUM_CONFIDENCE
23
+ expect(described_class::MEDIUM_CONFIDENCE).to be < described_class::HIGH_CONFIDENCE
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Imagination::Helpers::Scenario do
4
+ subject(:scenario) { described_class.new(action: 'deploy', context: { env: :prod }) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a UUID' do
8
+ expect(scenario.scenario_id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores action and context' do
12
+ expect(scenario.action).to eq('deploy')
13
+ expect(scenario.context[:env]).to eq(:prod)
14
+ end
15
+ end
16
+
17
+ describe '#add_outcome' do
18
+ it 'adds an outcome' do
19
+ scenario.add_outcome(type: :success, likelihood: 0.7, value: 0.8)
20
+ expect(scenario.outcomes.size).to eq(1)
21
+ expect(scenario.outcomes.first[:type]).to eq(:success)
22
+ end
23
+
24
+ it 'clamps likelihood to 0-1' do
25
+ scenario.add_outcome(likelihood: 1.5, value: 0.5)
26
+ expect(scenario.outcomes.first[:likelihood]).to eq(1.0)
27
+ end
28
+
29
+ it 'classifies valence' do
30
+ scenario.add_outcome(value: 0.8)
31
+ expect(scenario.outcomes.first[:valence]).to eq(:very_positive)
32
+ end
33
+ end
34
+
35
+ describe '#expected_value' do
36
+ it 'returns 0 with no outcomes' do
37
+ expect(scenario.expected_value).to eq(0.0)
38
+ end
39
+
40
+ it 'computes weighted expected value' do
41
+ scenario.add_outcome(likelihood: 0.7, value: 0.8)
42
+ scenario.add_outcome(likelihood: 0.3, value: -0.5)
43
+ ev = scenario.expected_value
44
+ expect(ev).to be_within(0.001).of((0.7 * 0.8) + (0.3 * -0.5))
45
+ end
46
+ end
47
+
48
+ describe '#risk_score' do
49
+ it 'returns 0 with no negative outcomes' do
50
+ scenario.add_outcome(likelihood: 0.5, value: 0.5)
51
+ expect(scenario.risk_score).to eq(0.0)
52
+ end
53
+
54
+ it 'computes risk from negative outcomes' do
55
+ scenario.add_outcome(likelihood: 0.3, value: -0.6)
56
+ expect(scenario.risk_score).to be_within(0.001).of(0.3 * 0.6)
57
+ end
58
+ end
59
+
60
+ describe '#best_outcome / #worst_outcome' do
61
+ before do
62
+ scenario.add_outcome(value: 0.8, likelihood: 0.5)
63
+ scenario.add_outcome(value: -0.3, likelihood: 0.5)
64
+ end
65
+
66
+ it 'returns highest value outcome as best' do
67
+ expect(scenario.best_outcome[:value]).to eq(0.8)
68
+ end
69
+
70
+ it 'returns lowest value outcome as worst' do
71
+ expect(scenario.worst_outcome[:value]).to eq(-0.3)
72
+ end
73
+ end
74
+
75
+ describe '#reversibility' do
76
+ it 'returns 1.0 when all outcomes are reversible' do
77
+ scenario.add_outcome(value: 0.5, reversible: true)
78
+ expect(scenario.reversibility).to eq(1.0)
79
+ end
80
+
81
+ it 'returns 0.0 when no outcomes are reversible' do
82
+ scenario.add_outcome(value: 0.5, reversible: false)
83
+ expect(scenario.reversibility).to eq(0.0)
84
+ end
85
+ end
86
+
87
+ describe '#evaluate' do
88
+ before do
89
+ scenario.add_outcome(likelihood: 0.7, value: 0.6)
90
+ scenario.add_outcome(likelihood: 0.3, value: -0.3)
91
+ end
92
+
93
+ it 'produces evaluation hash' do
94
+ scenario.evaluate
95
+ expect(scenario.evaluation).to include(:expected_value, :risk, :reversibility, :composite)
96
+ end
97
+
98
+ it 'produces a composite score' do
99
+ scenario.evaluate
100
+ expect(scenario.evaluation[:composite]).to be_a(Numeric)
101
+ expect(scenario.evaluation[:composite]).to be > 0
102
+ end
103
+ end
104
+
105
+ describe '#to_h' do
106
+ it 'returns complete scenario hash' do
107
+ h = scenario.to_h
108
+ expect(h).to include(:scenario_id, :action, :context, :outcomes, :expected_value)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Imagination::Helpers::SimulationStore do
4
+ subject(:store) { described_class.new }
5
+
6
+ let(:simulation) do
7
+ {
8
+ simulation_id: SecureRandom.uuid,
9
+ mode: :prospective,
10
+ scenarios: [],
11
+ recommendation: { action: 'deploy', composite: 0.8 },
12
+ simulated_at: Time.now.utc
13
+ }
14
+ end
15
+
16
+ describe '#store' do
17
+ it 'stores a simulation' do
18
+ store.store(simulation)
19
+ expect(store.size).to eq(1)
20
+ end
21
+
22
+ it 'caps at MAX_SIMULATIONS' do
23
+ (Legion::Extensions::Imagination::Helpers::Constants::MAX_SIMULATIONS + 5).times do |i|
24
+ store.store(simulation.merge(simulation_id: "sim-#{i}"))
25
+ end
26
+ expect(store.size).to eq(Legion::Extensions::Imagination::Helpers::Constants::MAX_SIMULATIONS)
27
+ end
28
+ end
29
+
30
+ describe '#get' do
31
+ it 'retrieves by ID' do
32
+ store.store(simulation)
33
+ found = store.get(simulation[:simulation_id])
34
+ expect(found).to eq(simulation)
35
+ end
36
+
37
+ it 'returns nil for missing ID' do
38
+ expect(store.get('nonexistent')).to be_nil
39
+ end
40
+ end
41
+
42
+ describe '#recent' do
43
+ it 'returns last N simulations' do
44
+ 3.times { |i| store.store(simulation.merge(simulation_id: "sim-#{i}")) }
45
+ expect(store.recent(limit: 2).size).to eq(2)
46
+ end
47
+ end
48
+
49
+ describe '#by_mode' do
50
+ it 'filters by mode' do
51
+ store.store(simulation.merge(mode: :prospective))
52
+ store.store(simulation.merge(simulation_id: 'x', mode: :counterfactual))
53
+ expect(store.by_mode(:prospective).size).to eq(1)
54
+ end
55
+ end
56
+
57
+ describe '#accuracy_check' do
58
+ it 'records actual outcome' do
59
+ store.store(simulation)
60
+ result = store.accuracy_check(simulation[:simulation_id], actual_outcome: { valence: :positive })
61
+ expect(result).to be true
62
+ end
63
+
64
+ it 'returns nil for missing simulation' do
65
+ result = store.accuracy_check('nonexistent', actual_outcome: {})
66
+ expect(result).to be_nil
67
+ end
68
+ end
69
+
70
+ describe '#simulation_accuracy' do
71
+ it 'returns nil with no checked simulations' do
72
+ expect(store.simulation_accuracy).to be_nil
73
+ end
74
+
75
+ it 'computes accuracy' do
76
+ store.store(simulation)
77
+ store.accuracy_check(simulation[:simulation_id], actual_outcome: { valence: :positive })
78
+ expect(store.simulation_accuracy).to eq(1.0)
79
+ end
80
+ end
81
+
82
+ describe '#clear' do
83
+ it 'removes all simulations' do
84
+ store.store(simulation)
85
+ store.clear
86
+ expect(store.size).to eq(0)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Imagination::Runners::Imagination do
4
+ let(:client) { Legion::Extensions::Imagination::Client.new }
5
+
6
+ describe '#simulate' do
7
+ it 'simulates multiple actions' do
8
+ result = client.simulate(actions: %w[deploy rollback wait])
9
+ expect(result[:scenarios].size).to eq(3)
10
+ expect(result).to have_key(:recommendation)
11
+ end
12
+
13
+ it 'limits to MAX_SCENARIOS' do
14
+ actions = (1..10).map { |i| "action_#{i}" }
15
+ result = client.simulate(actions: actions)
16
+ expect(result[:scenarios].size).to be <= Legion::Extensions::Imagination::Helpers::Constants::MAX_SCENARIOS
17
+ end
18
+
19
+ it 'stores simulation in history' do
20
+ client.simulate(actions: %w[deploy])
21
+ expect(client.simulation_store.size).to eq(1)
22
+ end
23
+
24
+ it 'recommends the best action' do
25
+ result = client.simulate(
26
+ actions: %w[safe_action risky_action],
27
+ context: { familiar: true, alignment: 0.8 },
28
+ risk_tolerance: :conservative
29
+ )
30
+ expect(result[:recommendation]).to have_key(:action)
31
+ expect(result[:recommendation]).to have_key(:composite)
32
+ end
33
+
34
+ it 'respects risk tolerance' do
35
+ conservative = client.simulate(actions: %w[deploy], risk_tolerance: :conservative)
36
+ aggressive = client.simulate(actions: %w[deploy], risk_tolerance: :aggressive)
37
+ expect(conservative[:recommendation]).not_to be_nil
38
+ expect(aggressive[:recommendation]).not_to be_nil
39
+ end
40
+ end
41
+
42
+ describe '#what_if' do
43
+ it 'returns scenario with consequence chain' do
44
+ result = client.what_if(action: 'merge_without_tests', depth: 2)
45
+ expect(result[:consequence_chain].size).to eq(3)
46
+ expect(result[:depth]).to eq(2)
47
+ end
48
+
49
+ it 'limits depth to MAX_DEPTH' do
50
+ result = client.what_if(action: 'test', depth: 100)
51
+ expect(result[:depth]).to eq(Legion::Extensions::Imagination::Helpers::Constants::MAX_DEPTH)
52
+ end
53
+
54
+ it 'returns overall valence' do
55
+ result = client.what_if(action: 'safe_deploy', context: { familiar: true })
56
+ expect(result[:overall_valence]).to be_a(Symbol)
57
+ end
58
+ end
59
+
60
+ describe '#compare' do
61
+ it 'compares two actions' do
62
+ result = client.compare(action_a: 'refactor', action_b: 'ship_now')
63
+ expect(%i[a b]).to include(result[:winner])
64
+ expect(result).to have_key(:margin)
65
+ expect(result).to have_key(:decisive)
66
+ end
67
+
68
+ it 'returns scenario details for both' do
69
+ result = client.compare(action_a: 'a', action_b: 'b')
70
+ expect(result[:scenario_a]).to have_key(:expected_value)
71
+ expect(result[:scenario_b]).to have_key(:expected_value)
72
+ end
73
+ end
74
+
75
+ describe '#record_actual_outcome' do
76
+ it 'records outcome for existing simulation' do
77
+ sim = client.simulate(actions: %w[deploy])
78
+ result = client.record_actual_outcome(
79
+ simulation_id: sim[:simulation_id],
80
+ actual_outcome: { valence: :positive }
81
+ )
82
+ expect(result[:accurate]).not_to be_nil
83
+ end
84
+
85
+ it 'returns error for missing simulation' do
86
+ result = client.record_actual_outcome(simulation_id: 'nonexistent', actual_outcome: {})
87
+ expect(result[:error]).to eq(:not_found)
88
+ end
89
+ end
90
+
91
+ describe '#imagination_stats' do
92
+ it 'returns stats summary' do
93
+ client.simulate(actions: %w[deploy])
94
+ result = client.imagination_stats
95
+ expect(result[:total_simulations]).to eq(1)
96
+ expect(result).to have_key(:by_mode)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Logging
5
+ module_function
6
+
7
+ def debug(*); end
8
+
9
+ def info(*); end
10
+
11
+ def warn(*); end
12
+
13
+ def error(*); end
14
+ end
15
+
16
+ module Extensions
17
+ module Helpers; end
18
+ end
19
+ end
20
+
21
+ require 'legion/extensions/imagination'
22
+
23
+ RSpec.configure do |config|
24
+ config.expect_with :rspec do |expectations|
25
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
26
+ end
27
+
28
+ config.mock_with :rspec do |mocks|
29
+ mocks.verify_partial_doubles = true
30
+ end
31
+
32
+ config.shared_context_metadata_behavior = :apply_to_host_groups
33
+ config.order = :random
34
+ Kernel.srand config.seed
35
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-imagination
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: Counterfactual simulation engine — mental rehearsal of actions before
27
+ committing
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - lex-imagination.gemspec
38
+ - lib/legion/extensions/imagination.rb
39
+ - lib/legion/extensions/imagination/client.rb
40
+ - lib/legion/extensions/imagination/helpers/constants.rb
41
+ - lib/legion/extensions/imagination/helpers/scenario.rb
42
+ - lib/legion/extensions/imagination/helpers/simulation_store.rb
43
+ - lib/legion/extensions/imagination/runners/imagination.rb
44
+ - lib/legion/extensions/imagination/version.rb
45
+ - spec/legion/extensions/imagination/client_spec.rb
46
+ - spec/legion/extensions/imagination/helpers/constants_spec.rb
47
+ - spec/legion/extensions/imagination/helpers/scenario_spec.rb
48
+ - spec/legion/extensions/imagination/helpers/simulation_store_spec.rb
49
+ - spec/legion/extensions/imagination/runners/imagination_spec.rb
50
+ - spec/spec_helper.rb
51
+ homepage: https://github.com/LegionIO/lex-imagination
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/LegionIO/lex-imagination
56
+ source_code_uri: https://github.com/LegionIO/lex-imagination
57
+ documentation_uri: https://github.com/LegionIO/lex-imagination
58
+ changelog_uri: https://github.com/LegionIO/lex-imagination
59
+ bug_tracker_uri: https://github.com/LegionIO/lex-imagination/issues
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: LEX Imagination
78
+ test_files: []