lex-temporal 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: 5f4a4b711b75ed1f26943f373aaebb70ee08274151a67c952a5894ba64cb45d2
4
+ data.tar.gz: 8b78d44cbde78674c8d7b8149da24ff36bd80b4b587e24d5d238715518586d3b
5
+ SHA512:
6
+ metadata.gz: 627401699a4d741f3db2e29533cd79eb01eb153fa4f7218b395a6148ebb8134db5fb692c7fb5e8ee2eb5638c5dbd06d1bf50e3adfbc05a719467bf6d5d5d8e70
7
+ data.tar.gz: ec4a7c83958e5b54da0fb0afc997653ff24e8ac141c90b809885a46d53350c333c40b781e9c0f400274ed99a2170de1aa857d25fe43ac61513261778b49d90d2
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ class Client
7
+ include Runners::Temporal
8
+
9
+ attr_reader :temporal_store
10
+
11
+ def initialize(temporal_store: nil, **)
12
+ @temporal_store = temporal_store || Helpers::TemporalStore.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ module Helpers
7
+ module Constants
8
+ # Time horizons for urgency classification
9
+ URGENCY_HORIZONS = {
10
+ immediate: 10, # seconds
11
+ short_term: 60, # 1 minute
12
+ medium: 300, # 5 minutes
13
+ long_term: 3600, # 1 hour
14
+ distant: 86_400 # 1 day
15
+ }.freeze
16
+
17
+ # Urgency levels (higher = more urgent)
18
+ URGENCY_LEVELS = %i[none low moderate high critical].freeze
19
+
20
+ # Deadline urgency thresholds (fraction of remaining time)
21
+ DEADLINE_URGENCY = {
22
+ critical: 0.1, # <= 10% time remaining
23
+ high: 0.25, # <= 25%
24
+ moderate: 0.5, # <= 50%
25
+ low: 0.75, # <= 75%
26
+ none: 1.0 # > 75%
27
+ }.freeze
28
+
29
+ # Temporal pattern types
30
+ PATTERN_TYPES = %i[periodic bursty circadian random].freeze
31
+
32
+ # Minimum occurrences to detect a pattern
33
+ MIN_PATTERN_OBSERVATIONS = 5
34
+
35
+ # Maximum tracked events per domain
36
+ MAX_EVENTS_PER_DOMAIN = 100
37
+
38
+ # Maximum active deadlines
39
+ MAX_DEADLINES = 50
40
+
41
+ # Maximum temporal patterns tracked
42
+ MAX_PATTERNS = 30
43
+
44
+ # How many recent events to keep globally
45
+ MAX_GLOBAL_EVENTS = 500
46
+
47
+ # Coefficient of variation threshold for periodic vs bursty
48
+ PERIODIC_CV_THRESHOLD = 0.3
49
+
50
+ # Burst detection: events within this multiplier of mean interval
51
+ BURST_MULTIPLIER = 0.25
52
+
53
+ # Subjective time dilation factor ranges
54
+ # When arousal is high, time feels slower (more ticks perceived)
55
+ # When bored/low arousal, time flies
56
+ DILATION_RANGE = { min: 0.5, max: 2.0 }.freeze
57
+
58
+ # EMA alpha for subjective time tracking
59
+ SUBJECTIVE_ALPHA = 0.2
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ module Helpers
7
+ class TemporalPattern
8
+ attr_reader :domain, :event, :pattern_type, :mean_interval,
9
+ :observation_count, :last_predicted, :accuracy_count, :total_predictions
10
+
11
+ def initialize(domain:, event:)
12
+ @domain = domain
13
+ @event = event
14
+ @intervals = []
15
+ @pattern_type = :random
16
+ @mean_interval = nil
17
+ @observation_count = 0
18
+ @last_predicted = nil
19
+ @accuracy_count = 0
20
+ @total_predictions = 0
21
+ end
22
+
23
+ def add_observation(timestamps)
24
+ return if timestamps.size < 2
25
+
26
+ @intervals = compute_intervals(timestamps)
27
+ @observation_count = timestamps.size
28
+ @mean_interval = @intervals.sum / @intervals.size.to_f
29
+ @pattern_type = classify_pattern
30
+ end
31
+
32
+ def predict_next(from: Time.now.utc)
33
+ return nil unless @mean_interval && @observation_count >= Constants::MIN_PATTERN_OBSERVATIONS
34
+
35
+ predicted = from + @mean_interval
36
+ @last_predicted = predicted
37
+ @total_predictions += 1
38
+ { predicted_at: predicted, confidence: prediction_confidence, pattern: @pattern_type }
39
+ end
40
+
41
+ def record_actual(actual_time)
42
+ return unless @last_predicted
43
+
44
+ error = (actual_time - @last_predicted).abs
45
+ tolerance = (@mean_interval || 60) * 0.3
46
+ @accuracy_count += 1 if error <= tolerance
47
+ end
48
+
49
+ def prediction_accuracy
50
+ return 0.0 if @total_predictions.zero?
51
+
52
+ @accuracy_count.to_f / @total_predictions
53
+ end
54
+
55
+ def periodic?
56
+ @pattern_type == :periodic
57
+ end
58
+
59
+ def bursty?
60
+ @pattern_type == :bursty
61
+ end
62
+
63
+ def to_h
64
+ {
65
+ domain: @domain,
66
+ event: @event,
67
+ pattern_type: @pattern_type,
68
+ mean_interval: @mean_interval,
69
+ observation_count: @observation_count,
70
+ accuracy: prediction_accuracy
71
+ }
72
+ end
73
+
74
+ private
75
+
76
+ def compute_intervals(timestamps)
77
+ sorted = timestamps.sort
78
+ sorted.each_cons(2).map { |a, b| b - a }
79
+ end
80
+
81
+ def classify_pattern
82
+ return :random if @intervals.size < Constants::MIN_PATTERN_OBSERVATIONS
83
+
84
+ cv = coefficient_of_variation
85
+ if cv < Constants::PERIODIC_CV_THRESHOLD
86
+ :periodic
87
+ elsif bursts?
88
+ :bursty
89
+ else
90
+ :random
91
+ end
92
+ end
93
+
94
+ def coefficient_of_variation
95
+ return Float::INFINITY if @intervals.empty? || @mean_interval.nil? || @mean_interval.zero?
96
+
97
+ std_dev = Math.sqrt(@intervals.map { |i| (i - @mean_interval)**2 }.sum / @intervals.size.to_f)
98
+ std_dev / @mean_interval
99
+ end
100
+
101
+ def bursts?
102
+ return false unless @mean_interval
103
+
104
+ burst_threshold = @mean_interval * Constants::BURST_MULTIPLIER
105
+ burst_count = @intervals.count { |i| i < burst_threshold }
106
+ burst_count > @intervals.size * 0.3
107
+ end
108
+
109
+ def prediction_confidence
110
+ base = case @pattern_type
111
+ when :periodic then 0.8
112
+ when :bursty then 0.4
113
+ else 0.2
114
+ end
115
+ base * [1.0, @observation_count / 10.0].min
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ module Helpers
7
+ class TemporalStore
8
+ attr_reader :perception, :patterns
9
+
10
+ def initialize
11
+ @perception = TimePerception.new
12
+ @patterns = {}
13
+ end
14
+
15
+ def record_event(event, domain: :general)
16
+ key = @perception.mark_event(event, domain: domain)
17
+ update_pattern(domain, event)
18
+ key
19
+ end
20
+
21
+ def elapsed(event, domain: :general)
22
+ @perception.elapsed_since(event, domain: domain)
23
+ end
24
+
25
+ def predict_next(event, domain: :general)
26
+ pattern_key = "#{domain}:#{event}"
27
+ pattern = @patterns[pattern_key]
28
+ return nil unless pattern
29
+
30
+ pattern.predict_next
31
+ end
32
+
33
+ def record_prediction_outcome(event, actual_time:, domain: :general)
34
+ pattern_key = "#{domain}:#{event}"
35
+ pattern = @patterns[pattern_key]
36
+ pattern&.record_actual(actual_time)
37
+ end
38
+
39
+ def detect_patterns_for(event, domain: :general)
40
+ pattern_key = "#{domain}:#{event}"
41
+ @patterns[pattern_key]&.to_h
42
+ end
43
+
44
+ def all_patterns
45
+ @patterns.values.map(&:to_h)
46
+ end
47
+
48
+ def periodic_patterns
49
+ @patterns.values.select(&:periodic?).map(&:to_h)
50
+ end
51
+
52
+ def temporal_summary
53
+ {
54
+ perception: @perception.to_h,
55
+ pattern_count: @patterns.size,
56
+ periodic_count: @patterns.values.count(&:periodic?),
57
+ bursty_count: @patterns.values.count(&:bursty?),
58
+ overall_urgency: @perception.overall_urgency,
59
+ overdue_deadlines: @perception.overdue_deadlines.size
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def update_pattern(domain, event)
66
+ pattern_key = "#{domain}:#{event}"
67
+ event_key = "#{domain}:#{event}"
68
+ timestamps = @perception.events[event_key]
69
+ return unless timestamps && timestamps.size >= 2
70
+
71
+ @patterns[pattern_key] ||= TemporalPattern.new(domain: domain, event: event)
72
+ @patterns[pattern_key].add_observation(timestamps)
73
+ evict_patterns_if_needed
74
+ end
75
+
76
+ def evict_patterns_if_needed
77
+ return unless @patterns.size > Constants::MAX_PATTERNS
78
+
79
+ weakest = @patterns.min_by { |_k, p| p.observation_count }
80
+ @patterns.delete(weakest.first) if weakest
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ module Helpers
7
+ class TimePerception
8
+ attr_reader :events, :deadlines, :subjective_rate
9
+
10
+ def initialize
11
+ @events = {}
12
+ @deadlines = {}
13
+ @subjective_rate = 1.0
14
+ @tick_count = 0
15
+ end
16
+
17
+ def mark_event(event, domain: :general)
18
+ key = "#{domain}:#{event}"
19
+ @events[key] ||= []
20
+ @events[key] << Time.now.utc
21
+ trim_events(key)
22
+ key
23
+ end
24
+
25
+ def elapsed_since(event, domain: :general)
26
+ key = "#{domain}:#{event}"
27
+ timestamps = @events[key]
28
+ return nil unless timestamps&.any?
29
+
30
+ Time.now.utc - timestamps.last
31
+ end
32
+
33
+ def time_since_first(event, domain: :general)
34
+ key = "#{domain}:#{event}"
35
+ timestamps = @events[key]
36
+ return nil unless timestamps&.any?
37
+
38
+ Time.now.utc - timestamps.first
39
+ end
40
+
41
+ def event_count(event, domain: :general)
42
+ key = "#{domain}:#{event}"
43
+ @events.fetch(key, []).size
44
+ end
45
+
46
+ def set_deadline(id, at:, description: nil)
47
+ @deadlines[id] = { at: at, created: Time.now.utc, description: description }
48
+ evict_deadlines_if_needed
49
+ id
50
+ end
51
+
52
+ def remove_deadline(id)
53
+ @deadlines.delete(id)
54
+ end
55
+
56
+ def deadline_urgency(id)
57
+ dl = @deadlines[id]
58
+ return nil unless dl
59
+
60
+ remaining = dl[:at] - Time.now.utc
61
+ return :overdue if remaining <= 0
62
+
63
+ total = dl[:at] - dl[:created]
64
+ return :critical if total <= 0
65
+
66
+ fraction = remaining / total
67
+ classify_deadline_urgency(fraction)
68
+ end
69
+
70
+ def overdue_deadlines
71
+ now = Time.now.utc
72
+ @deadlines.select { |_id, dl| dl[:at] <= now }
73
+ .map { |id, dl| { id: id, overdue_by: now - dl[:at], description: dl[:description] } }
74
+ end
75
+
76
+ def upcoming_deadlines(within: 3600)
77
+ now = Time.now.utc
78
+ cutoff = now + within
79
+ @deadlines.select { |_id, dl| dl[:at] > now && dl[:at] <= cutoff }
80
+ .map { |id, dl| { id: id, remaining: dl[:at] - now, description: dl[:description] } }
81
+ .sort_by { |d| d[:remaining] }
82
+ end
83
+
84
+ def update_subjective_time(arousal: 0.5, cognitive_load: 0.5, **)
85
+ @tick_count += 1
86
+ raw_dilation = compute_dilation(arousal, cognitive_load)
87
+ @subjective_rate = ema(@subjective_rate, raw_dilation, Constants::SUBJECTIVE_ALPHA)
88
+ @subjective_rate
89
+ end
90
+
91
+ def subjective_elapsed(real_seconds)
92
+ real_seconds * @subjective_rate
93
+ end
94
+
95
+ def overall_urgency
96
+ urgencies = @deadlines.keys.map { |id| deadline_urgency(id) }.compact
97
+ return :none if urgencies.empty?
98
+
99
+ priority = Constants::URGENCY_LEVELS
100
+ urgencies.min_by { |u| u == :overdue ? -1 : priority.index(u) || 99 }
101
+ end
102
+
103
+ def to_h
104
+ {
105
+ event_domains: @events.size,
106
+ active_deadlines: @deadlines.size,
107
+ overdue_count: overdue_deadlines.size,
108
+ subjective_rate: @subjective_rate,
109
+ overall_urgency: overall_urgency,
110
+ tick_count: @tick_count
111
+ }
112
+ end
113
+
114
+ private
115
+
116
+ def trim_events(key)
117
+ return unless @events[key].size > Constants::MAX_EVENTS_PER_DOMAIN
118
+
119
+ @events[key] = @events[key].last(Constants::MAX_EVENTS_PER_DOMAIN)
120
+ end
121
+
122
+ def evict_deadlines_if_needed
123
+ return unless @deadlines.size > Constants::MAX_DEADLINES
124
+
125
+ oldest_key = @deadlines.min_by { |_id, dl| dl[:created] }.first
126
+ @deadlines.delete(oldest_key)
127
+ end
128
+
129
+ def classify_deadline_urgency(fraction)
130
+ Constants::DEADLINE_URGENCY.each do |level, threshold|
131
+ return level if fraction <= threshold
132
+ end
133
+ :none
134
+ end
135
+
136
+ def compute_dilation(arousal, cognitive_load)
137
+ raw = 1.0 + ((arousal - 0.5) * 1.0) + ((cognitive_load - 0.5) * 0.5)
138
+ raw.clamp(Constants::DILATION_RANGE[:min], Constants::DILATION_RANGE[:max])
139
+ end
140
+
141
+ def ema(current, observed, alpha)
142
+ (current * (1.0 - alpha)) + (observed * alpha)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ module Runners
7
+ module Temporal
8
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
9
+
10
+ def mark_event(event:, domain: :general, **)
11
+ key = temporal_store.record_event(event, domain: domain)
12
+ count = temporal_store.perception.event_count(event, domain: domain)
13
+ { marked: true, key: key, event: event, domain: domain, total_occurrences: count }
14
+ end
15
+
16
+ def elapsed_since(event:, domain: :general, **)
17
+ elapsed = temporal_store.elapsed(event, domain: domain)
18
+ if elapsed
19
+ { event: event, domain: domain, elapsed_seconds: elapsed, human: humanize_duration(elapsed) }
20
+ else
21
+ { event: event, domain: domain, elapsed_seconds: nil, reason: :no_record }
22
+ end
23
+ end
24
+
25
+ def set_deadline(id:, at:, description: nil, **)
26
+ temporal_store.perception.set_deadline(id, at: at, description: description)
27
+ { set: true, id: id, at: at, description: description }
28
+ end
29
+
30
+ def check_deadlines(**)
31
+ {
32
+ overdue: temporal_store.perception.overdue_deadlines,
33
+ upcoming: temporal_store.perception.upcoming_deadlines,
34
+ urgency: temporal_store.perception.overall_urgency,
35
+ total: temporal_store.perception.deadlines.size
36
+ }
37
+ end
38
+
39
+ def update_time_perception(tick_results: {}, **)
40
+ arousal = extract_arousal(tick_results)
41
+ load = extract_cognitive_load(tick_results)
42
+ rate = temporal_store.perception.update_subjective_time(arousal: arousal, cognitive_load: load)
43
+ {
44
+ subjective_rate: rate,
45
+ interpretation: interpret_rate(rate),
46
+ overall_urgency: temporal_store.perception.overall_urgency,
47
+ overdue_count: temporal_store.perception.overdue_deadlines.size
48
+ }
49
+ end
50
+
51
+ def predict_event(event:, domain: :general, **)
52
+ prediction = temporal_store.predict_next(event, domain: domain)
53
+ if prediction
54
+ { prediction: prediction, event: event, domain: domain }
55
+ else
56
+ { prediction: nil, reason: :insufficient_data }
57
+ end
58
+ end
59
+
60
+ def temporal_patterns(**)
61
+ {
62
+ all: temporal_store.all_patterns,
63
+ periodic: temporal_store.periodic_patterns,
64
+ count: temporal_store.patterns.size
65
+ }
66
+ end
67
+
68
+ def temporal_stats(**)
69
+ temporal_store.temporal_summary
70
+ end
71
+
72
+ private
73
+
74
+ def extract_arousal(tick_results)
75
+ emotion = tick_results[:emotional_evaluation]
76
+ return 0.5 unless emotion.is_a?(Hash)
77
+
78
+ emotion[:arousal] || 0.5
79
+ end
80
+
81
+ def extract_cognitive_load(tick_results)
82
+ elapsed = tick_results[:elapsed] || 0
83
+ budget = tick_results[:budget] || 1
84
+ return 0.5 if budget.zero?
85
+
86
+ (elapsed.to_f / budget).clamp(0.0, 1.0)
87
+ end
88
+
89
+ def humanize_duration(seconds)
90
+ if seconds < 60
91
+ "#{seconds.round(1)}s"
92
+ elsif seconds < 3600
93
+ "#{(seconds / 60).round(1)}m"
94
+ elsif seconds < 86_400
95
+ "#{(seconds / 3600).round(1)}h"
96
+ else
97
+ "#{(seconds / 86_400).round(1)}d"
98
+ end
99
+ end
100
+
101
+ def interpret_rate(rate)
102
+ if rate > 1.5
103
+ :time_crawling
104
+ elsif rate > 1.1
105
+ :time_slow
106
+ elsif rate < 0.7
107
+ :time_flying
108
+ elsif rate < 0.9
109
+ :time_fast
110
+ else
111
+ :normal
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Temporal
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'temporal/version'
4
+ require_relative 'temporal/helpers/constants'
5
+ require_relative 'temporal/helpers/time_perception'
6
+ require_relative 'temporal/helpers/temporal_pattern'
7
+ require_relative 'temporal/helpers/temporal_store'
8
+ require_relative 'temporal/runners/temporal'
9
+ require_relative 'temporal/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Temporal
14
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-temporal
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: 'Provides time-aware cognitive processing: elapsed awareness, urgency,
27
+ temporal patterns, and deadline tracking'
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/temporal.rb
35
+ - lib/legion/extensions/temporal/client.rb
36
+ - lib/legion/extensions/temporal/helpers/constants.rb
37
+ - lib/legion/extensions/temporal/helpers/temporal_pattern.rb
38
+ - lib/legion/extensions/temporal/helpers/temporal_store.rb
39
+ - lib/legion/extensions/temporal/helpers/time_perception.rb
40
+ - lib/legion/extensions/temporal/runners/temporal.rb
41
+ - lib/legion/extensions/temporal/version.rb
42
+ homepage: https://github.com/LegionIO/lex-temporal
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.6.9
62
+ specification_version: 4
63
+ summary: Temporal perception and time reasoning for LegionIO
64
+ test_files: []