lex-prospection 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: de2a689e0894969a5cfa8c7abff2cf2a95273b80886d0a77f52f1f7ef38cf408
4
+ data.tar.gz: 7d5dbf56573144226f438724b93a26f2c6ec030a893beb3b433554eb54578e6c
5
+ SHA512:
6
+ metadata.gz: 63e196a84901637427facc533a3d468759c31d3c8e14e3c6b5304fe3263d418e1568bbffe918f4f5028a51ea10e26eebbe76fbfaa76fd714f33d9cb3aa33b67e
7
+ data.tar.gz: c1a256a883a33ad6af03ce4026c48a2d399ae1af57532318c02f2dd4a9afcc97017f4bb62f21eb1e56b3d1b7de3865f2939d0d411946f337405cf1feddaa48f7
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # lex-prospection
2
+
3
+ Mental time travel and future scenario simulation for the LegionIO cognitive architecture. Imagines future events with emotional predictions and tracks forecast accuracy.
4
+
5
+ ## What It Does
6
+
7
+ Simulates future scenarios with predicted emotional valence and arousal. Applies two cognitive bias corrections: impact bias correction (reduces overestimation of emotional intensity) and focalism discount (reduces confidence when multiple scenarios compete for attention in the same domain). When scenarios are resolved against actual outcomes, forecast accuracy is tracked per domain.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ client = Legion::Extensions::Prospection::Client.new
13
+
14
+ # Imagine a future scenario
15
+ scenario = client.imagine_future(
16
+ domain: :deployment,
17
+ description: 'Deploy new service to production',
18
+ time_horizon: 7, # days from now
19
+ predicted_valence: 0.6, # expecting positive outcome
20
+ predicted_arousal: 0.7, # anticipation is high
21
+ confidence: 0.7
22
+ )
23
+ # => { success: true, scenario_id: '...', label: :positive,
24
+ # corrected_valence: 0.36, corrected_arousal: 0.42,
25
+ # confidence: 0.7, confidence_label: :moderate }
26
+
27
+ # See upcoming scenarios
28
+ client.near_future_scenarios(days: 7)
29
+ client.vivid_scenarios(count: 5)
30
+
31
+ # Resolve after outcome observed
32
+ client.resolve_future(
33
+ scenario_id: scenario[:scenario_id],
34
+ actual_valence: 0.7,
35
+ actual_arousal: 0.4
36
+ )
37
+ # => { success: true, forecast_error: 0.17, actual_valence: 0.7, actual_arousal: 0.4 }
38
+
39
+ # Check forecast accuracy for domain
40
+ client.forecast_accuracy(domain: :deployment)
41
+ # => { success: true, domain: :deployment, accuracy: 0.83 }
42
+
43
+ # Periodic decay (vividness fades)
44
+ client.update_prospection
45
+ client.prospection_stats
46
+ ```
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ bundle install
52
+ bundle exec rspec
53
+ bundle exec rubocop
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Prospection
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Prospection::Runners::Prospection
12
+ end
13
+
14
+ def runner_function
15
+ 'update_prospection'
16
+ end
17
+
18
+ def time
19
+ 300
20
+ end
21
+
22
+ def run_now?
23
+ false
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/prospection/helpers/constants'
4
+ require 'legion/extensions/prospection/helpers/scenario'
5
+ require 'legion/extensions/prospection/helpers/prospection_engine'
6
+ require 'legion/extensions/prospection/runners/prospection'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Prospection
11
+ class Client
12
+ include Runners::Prospection
13
+
14
+ attr_reader :prospection_engine
15
+
16
+ def initialize(prospection_engine: nil, **)
17
+ @prospection_engine = prospection_engine || Helpers::ProspectionEngine.new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Prospection
6
+ module Helpers
7
+ module Constants
8
+ MAX_SCENARIOS = 100
9
+ MAX_FORECASTS_PER_SCENARIO = 10
10
+ MAX_HISTORY = 200
11
+ SCENARIO_DECAY = 0.01
12
+ DEFAULT_CONFIDENCE = 0.4
13
+ IMPACT_BIAS_CORRECTION = 0.6
14
+ FOCALISM_DISCOUNT = 0.15
15
+ TEMPORAL_DISCOUNT_RATE = 0.05
16
+ MAX_TIME_HORIZON = 365
17
+ VIVIDNESS_ALPHA = 0.1
18
+ DEFAULT_VIVIDNESS = 0.5
19
+
20
+ VALENCE_LABELS = {
21
+ (0.6..) => :positive,
22
+ (0.2...0.6) => :neutral,
23
+ (-0.2...0.2) => :ambivalent,
24
+ (..-0.2) => :negative
25
+ }.freeze
26
+
27
+ CONFIDENCE_LABELS = {
28
+ (0.8..) => :calibrated,
29
+ (0.5...0.8) => :moderate,
30
+ (0.2...0.5) => :rough,
31
+ (..0.2) => :speculative
32
+ }.freeze
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Prospection
6
+ module Helpers
7
+ class ProspectionEngine
8
+ include Constants
9
+
10
+ attr_reader :scenarios, :domain_accuracy, :history
11
+
12
+ def initialize
13
+ @scenarios = {}
14
+ @domain_accuracy = {}
15
+ @history = []
16
+ end
17
+
18
+ def imagine(domain:, description:, time_horizon:, predicted_valence:,
19
+ predicted_arousal:, confidence: nil, **)
20
+ prune_if_full
21
+ scenario = Scenario.new(
22
+ domain: domain,
23
+ description: description,
24
+ time_horizon: time_horizon,
25
+ predicted_valence: predicted_valence,
26
+ predicted_arousal: predicted_arousal,
27
+ confidence: confidence
28
+ )
29
+ apply_focalism(scenario, domain)
30
+ @scenarios[scenario.id] = scenario
31
+ scenario
32
+ end
33
+
34
+ def scenarios_for(domain:)
35
+ @scenarios.values.select { |s| s.domain == domain && !s.resolved? }
36
+ end
37
+
38
+ def resolve_scenario(id:, actual_valence:, actual_arousal:)
39
+ scenario = @scenarios[id]
40
+ return nil unless scenario
41
+ return nil if scenario.resolved?
42
+
43
+ scenario.resolve(actual_valence: actual_valence, actual_arousal: actual_arousal)
44
+ update_domain_accuracy(scenario)
45
+ record_history(scenario)
46
+ scenario
47
+ end
48
+
49
+ def accuracy_for(domain)
50
+ 1.0 - @domain_accuracy.fetch(domain, 0.0)
51
+ end
52
+
53
+ def near_future(days: 7)
54
+ @scenarios.values.select { |s| !s.resolved? && s.time_horizon <= days }
55
+ end
56
+
57
+ def most_vivid(count: 5)
58
+ @scenarios.values
59
+ .reject(&:resolved?)
60
+ .sort_by { |s| -s.vividness }
61
+ .first(count)
62
+ end
63
+
64
+ def decay_all
65
+ @scenarios.each_value(&:decay)
66
+ @scenarios.reject! { |_, s| s.expired? }
67
+ end
68
+
69
+ def remove_scenario(id:)
70
+ @scenarios.delete(id)
71
+ end
72
+
73
+ def scenario_count
74
+ @scenarios.size
75
+ end
76
+
77
+ def domain_count
78
+ @scenarios.values.map(&:domain).uniq.size
79
+ end
80
+
81
+ def to_h
82
+ {
83
+ scenario_count: scenario_count,
84
+ domain_count: domain_count,
85
+ history_size: @history.size,
86
+ domain_accuracy: @domain_accuracy.transform_values { |v| (1.0 - v).round(4) }
87
+ }
88
+ end
89
+
90
+ private
91
+
92
+ def apply_focalism(scenario, domain)
93
+ active = scenarios_for(domain: domain)
94
+ scenario.apply_focalism_discount if active.size >= 1
95
+ end
96
+
97
+ def update_domain_accuracy(scenario)
98
+ error = scenario.forecast_error || 0.0
99
+ existing = @domain_accuracy.fetch(scenario.domain, error)
100
+ @domain_accuracy[scenario.domain] = existing + (VIVIDNESS_ALPHA * (error - existing))
101
+ end
102
+
103
+ def record_history(scenario)
104
+ @history << {
105
+ scenario_id: scenario.id,
106
+ domain: scenario.domain,
107
+ forecast_error: scenario.forecast_error,
108
+ resolved_at: scenario.resolved_at
109
+ }
110
+ @history.shift while @history.size > MAX_HISTORY
111
+ end
112
+
113
+ def prune_if_full
114
+ return unless @scenarios.size >= MAX_SCENARIOS
115
+
116
+ oldest = @scenarios.values
117
+ .reject(&:resolved?)
118
+ .min_by(&:created_at)
119
+ @scenarios.delete(oldest.id) if oldest
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Prospection
8
+ module Helpers
9
+ class Scenario
10
+ include Constants
11
+
12
+ attr_reader :id, :domain, :description, :time_horizon, :predicted_valence,
13
+ :predicted_arousal, :confidence, :vividness, :created_at,
14
+ :actual_valence, :actual_arousal, :resolved_at
15
+
16
+ def initialize(domain:, description:, time_horizon:, predicted_valence:,
17
+ predicted_arousal:, confidence: nil, **)
18
+ @id = SecureRandom.uuid
19
+ @domain = domain
20
+ @description = description
21
+ @time_horizon = [time_horizon.to_f, MAX_TIME_HORIZON].min
22
+ @predicted_valence = predicted_valence.clamp(-1.0, 1.0)
23
+ @predicted_arousal = predicted_arousal.clamp(0.0, 1.0)
24
+ @confidence = (confidence || DEFAULT_CONFIDENCE).clamp(0.0, 1.0)
25
+ @vividness = DEFAULT_VIVIDNESS
26
+ @created_at = Time.now.utc
27
+ @actual_valence = nil
28
+ @actual_arousal = nil
29
+ @resolved_at = nil
30
+ end
31
+
32
+ def temporal_discount
33
+ 1.0 - (TEMPORAL_DISCOUNT_RATE * @time_horizon).clamp(0.0, 0.9)
34
+ end
35
+
36
+ def corrected_valence
37
+ @predicted_valence * IMPACT_BIAS_CORRECTION * temporal_discount
38
+ end
39
+
40
+ def corrected_arousal
41
+ @predicted_arousal * IMPACT_BIAS_CORRECTION * temporal_discount
42
+ end
43
+
44
+ def label
45
+ VALENCE_LABELS.each do |range, lbl|
46
+ return lbl if range.cover?(corrected_valence)
47
+ end
48
+ :ambivalent
49
+ end
50
+
51
+ def confidence_label
52
+ CONFIDENCE_LABELS.each do |range, lbl|
53
+ return lbl if range.cover?(@confidence)
54
+ end
55
+ :speculative
56
+ end
57
+
58
+ def decay
59
+ @confidence = (@confidence - SCENARIO_DECAY).clamp(0.0, 1.0)
60
+ @vividness = (@vividness - SCENARIO_DECAY).clamp(0.0, 1.0)
61
+ end
62
+
63
+ def reinforce_vividness(amount)
64
+ @vividness = (@vividness + (VIVIDNESS_ALPHA * amount)).clamp(0.0, 1.0)
65
+ end
66
+
67
+ def apply_focalism_discount
68
+ @predicted_valence *= (1.0 - FOCALISM_DISCOUNT)
69
+ @predicted_arousal *= (1.0 - FOCALISM_DISCOUNT)
70
+ end
71
+
72
+ def resolve(actual_valence:, actual_arousal:)
73
+ @actual_valence = actual_valence.clamp(-1.0, 1.0)
74
+ @actual_arousal = actual_arousal.clamp(0.0, 1.0)
75
+ @resolved_at = Time.now.utc
76
+ end
77
+
78
+ def resolved?
79
+ !@resolved_at.nil?
80
+ end
81
+
82
+ def forecast_error
83
+ return nil unless resolved?
84
+
85
+ (corrected_valence - @actual_valence).abs
86
+ end
87
+
88
+ def expired?
89
+ @confidence < 0.01 && @vividness < 0.01
90
+ end
91
+
92
+ def to_h
93
+ {
94
+ id: @id,
95
+ domain: @domain,
96
+ description: @description,
97
+ time_horizon: @time_horizon,
98
+ predicted_valence: @predicted_valence,
99
+ predicted_arousal: @predicted_arousal,
100
+ corrected_valence: corrected_valence.round(4),
101
+ corrected_arousal: corrected_arousal.round(4),
102
+ confidence: @confidence.round(4),
103
+ confidence_label: confidence_label,
104
+ vividness: @vividness.round(4),
105
+ label: label,
106
+ resolved: resolved?,
107
+ forecast_error: forecast_error&.round(4),
108
+ created_at: @created_at,
109
+ resolved_at: @resolved_at
110
+ }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Prospection
6
+ module Runners
7
+ module Prospection
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def imagine_future(domain:, description:, time_horizon:, predicted_valence:,
12
+ predicted_arousal:, confidence: nil, **)
13
+ scenario = prospection_engine.imagine(
14
+ domain: domain,
15
+ description: description,
16
+ time_horizon: time_horizon,
17
+ predicted_valence: predicted_valence,
18
+ predicted_arousal: predicted_arousal,
19
+ confidence: confidence
20
+ )
21
+ Legion::Logging.debug \
22
+ "[prospection] imagine: domain=#{domain} horizon=#{time_horizon}d " \
23
+ "valence=#{predicted_valence.round(3)} label=#{scenario.label}"
24
+ {
25
+ success: true,
26
+ scenario_id: scenario.id,
27
+ domain: scenario.domain,
28
+ label: scenario.label,
29
+ corrected_valence: scenario.corrected_valence.round(4),
30
+ corrected_arousal: scenario.corrected_arousal.round(4),
31
+ confidence: scenario.confidence.round(4),
32
+ confidence_label: scenario.confidence_label
33
+ }
34
+ end
35
+
36
+ def resolve_future(scenario_id:, actual_valence:, actual_arousal:, **)
37
+ scenario = prospection_engine.resolve_scenario(
38
+ id: scenario_id,
39
+ actual_valence: actual_valence,
40
+ actual_arousal: actual_arousal
41
+ )
42
+ unless scenario
43
+ Legion::Logging.debug "[prospection] resolve: id=#{scenario_id} not_found_or_already_resolved"
44
+ return { success: false, reason: :not_found }
45
+ end
46
+ Legion::Logging.debug \
47
+ "[prospection] resolve: id=#{scenario_id} domain=#{scenario.domain} " \
48
+ "error=#{scenario.forecast_error&.round(3)}"
49
+ {
50
+ success: true,
51
+ scenario_id: scenario.id,
52
+ domain: scenario.domain,
53
+ forecast_error: scenario.forecast_error&.round(4),
54
+ actual_valence: actual_valence,
55
+ actual_arousal: actual_arousal
56
+ }
57
+ end
58
+
59
+ def forecast_accuracy(domain: :general, **)
60
+ accuracy = prospection_engine.accuracy_for(domain)
61
+ Legion::Logging.debug "[prospection] accuracy: domain=#{domain} accuracy=#{accuracy.round(3)}"
62
+ { success: true, domain: domain, accuracy: accuracy.round(4) }
63
+ end
64
+
65
+ def near_future_scenarios(days: 7, **)
66
+ scenarios = prospection_engine.near_future(days: days)
67
+ Legion::Logging.debug "[prospection] near_future: days=#{days} count=#{scenarios.size}"
68
+ {
69
+ success: true,
70
+ days: days,
71
+ scenarios: scenarios.map(&:to_h),
72
+ count: scenarios.size
73
+ }
74
+ end
75
+
76
+ def vivid_scenarios(count: 5, **)
77
+ scenarios = prospection_engine.most_vivid(count: count)
78
+ Legion::Logging.debug "[prospection] vivid: count=#{scenarios.size}"
79
+ {
80
+ success: true,
81
+ scenarios: scenarios.map(&:to_h),
82
+ count: scenarios.size
83
+ }
84
+ end
85
+
86
+ def scenarios_in_domain(domain:, **)
87
+ scenarios = prospection_engine.scenarios_for(domain: domain)
88
+ Legion::Logging.debug "[prospection] domain_scenarios: domain=#{domain} count=#{scenarios.size}"
89
+ {
90
+ success: true,
91
+ domain: domain,
92
+ scenarios: scenarios.map(&:to_h),
93
+ count: scenarios.size
94
+ }
95
+ end
96
+
97
+ def update_prospection(**)
98
+ prospection_engine.decay_all
99
+ stats = prospection_engine.to_h
100
+ Legion::Logging.debug \
101
+ "[prospection] tick: scenarios=#{stats[:scenario_count]} " \
102
+ "domains=#{stats[:domain_count]} history=#{stats[:history_size]}"
103
+ { success: true }.merge(stats)
104
+ end
105
+
106
+ def prospection_stats(**)
107
+ stats = prospection_engine.to_h
108
+ Legion::Logging.debug \
109
+ "[prospection] stats: scenarios=#{stats[:scenario_count]} " \
110
+ "domains=#{stats[:domain_count]}"
111
+ { success: true, stats: stats }
112
+ end
113
+
114
+ private
115
+
116
+ def prospection_engine
117
+ @prospection_engine ||= Helpers::ProspectionEngine.new
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Prospection
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/prospection/version'
5
+ require 'legion/extensions/prospection/helpers/constants'
6
+ require 'legion/extensions/prospection/helpers/scenario'
7
+ require 'legion/extensions/prospection/helpers/prospection_engine'
8
+ require 'legion/extensions/prospection/runners/prospection'
9
+ require 'legion/extensions/prospection/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Prospection
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-prospection
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
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: 'Future-oriented mental simulation: imagines future scenarios, predicts
27
+ their emotional impact, tracks impact bias and focalism corrections, and calibrates
28
+ forecast accuracy over time. Based on Gilbert & Wilson affective forecasting research.'
29
+ email:
30
+ - matt@iverson.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - lib/legion/extensions/prospection.rb
37
+ - lib/legion/extensions/prospection/actors/decay.rb
38
+ - lib/legion/extensions/prospection/client.rb
39
+ - lib/legion/extensions/prospection/helpers/constants.rb
40
+ - lib/legion/extensions/prospection/helpers/prospection_engine.rb
41
+ - lib/legion/extensions/prospection/helpers/scenario.rb
42
+ - lib/legion/extensions/prospection/runners/prospection.rb
43
+ - lib/legion/extensions/prospection/version.rb
44
+ homepage: https://github.com/LegionIO/lex-prospection
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.4'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: Affective forecasting and mental time travel for LegionIO
66
+ test_files: []