lex-context 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: a471dc0eb03821a6242b22bf9d1355d2aca38b6152498dd473c56ee96b3c0013
4
+ data.tar.gz: 83023441c865a754e6a3397750a7e95f050181d9f0fe8650f0c0a7830ed9d094
5
+ SHA512:
6
+ metadata.gz: 87c59f574c3683a53f7b3e9018a9a2205fe916099dde07cc984155bd6ed2270cd1448f6ed87db6868ddfbf35525219044516df260f73312d0bdbeb0428cba944
7
+ data.tar.gz: aae221c9aee738367fd76f7c4052cbb81dcfed652052ec6cf53fe1f736dbec0cf5c4962587d1916237f697d23fa5ea00d4e081325d261f6d08c35401e4bb51db
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # lex-context
2
+
3
+ A LegionIO cognitive architecture extension for contextual framing and situation modeling. Maintains context frames that shape perception, memory retrieval, and action selection. Tracks context switches with measurable switch costs and familiarity-based discounts — switching to a familiar frame costs less than switching to an unfamiliar one.
4
+
5
+ ## What It Does
6
+
7
+ Manages a registry of **frames** — each representing a situated context with a domain, a set of cues (tokens that activate the frame), and a strength score. An active frame stack (up to 10 deep) tracks the current cognitive situation.
8
+
9
+ - **Create frames** representing different operational contexts (coding, reviewing, planning)
10
+ - **Activate frames** explicitly or let `auto_switch` select the best match from input cues
11
+ - **Measure switch costs** — base 0.15 cost reduced by familiarity and same-domain bonuses, increased by rapid successive switches
12
+ - **Decay frames** over time — unused frames weaken and are eventually pruned from the registry
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ require 'lex-context'
18
+
19
+ client = Legion::Extensions::Context::Client.new
20
+
21
+ # Create context frames
22
+ coding = client.create_context(
23
+ name: 'software_development',
24
+ domain: :technical,
25
+ cues: [:code, :function, :test, :deploy, :bug]
26
+ )
27
+ # => { success: true, frame: { id: "uuid...", name: "software_development",
28
+ # domain: :technical, strength: 1.0, familiarity: 0.3, label: :dominant, ... } }
29
+
30
+ review = client.create_context(
31
+ name: 'code_review',
32
+ domain: :technical,
33
+ cues: [:review, :pr, :feedback, :approve, :merge]
34
+ )
35
+
36
+ frame_id = coding[:frame][:id]
37
+
38
+ # Activate a frame explicitly
39
+ client.activate_context(frame_id: frame_id)
40
+ # => { success: true, frame: { ... }, switch_cost: 0.0 }
41
+
42
+ # Detect matching frames from input cues
43
+ client.detect_context(input_cues: [:review, :pr, :feedback])
44
+ # => { success: true, candidates: [{ frame: {...}, relevance: 0.6 }],
45
+ # count: 1, best: { name: "code_review", relevance: 0.6 } }
46
+
47
+ # Automatically switch to the best-matching frame
48
+ client.auto_switch(input_cues: [:review, :pr])
49
+ # => { success: true, switched: true, frame: { ... }, switch_cost: 0.12 }
50
+
51
+ # Current active frame
52
+ client.current_context
53
+ # => { success: true, frame: { name: "code_review", ... } }
54
+
55
+ # Add a cue to an existing frame
56
+ client.add_cue(frame_id: frame_id, cue: :refactor)
57
+ # => { success: true, frame: { ... } }
58
+
59
+ # Periodic maintenance: decay all frames, remove stale ones
60
+ client.update_context
61
+ # => { success: true, frame_count: 2, active: "code_review" }
62
+
63
+ # Stats
64
+ client.context_stats
65
+ # => { success: true, stats: { frame_count: 2, active_frame: "code_review",
66
+ # stack_depth: 1, switch_count: 1, avg_switch_cost: 0.12,
67
+ # by_domain: { technical: 2 } } }
68
+ ```
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ bundle install
74
+ bundle exec rspec
75
+ bundle exec rubocop
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/context/helpers/constants'
4
+ require 'legion/extensions/context/helpers/frame'
5
+ require 'legion/extensions/context/helpers/context_manager'
6
+ require 'legion/extensions/context/runners/context'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Context
11
+ class Client
12
+ include Runners::Context
13
+
14
+ attr_reader :context_manager
15
+
16
+ def initialize(context_manager: nil, **)
17
+ @context_manager = context_manager || Helpers::ContextManager.new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Context
6
+ module Helpers
7
+ module Constants
8
+ MAX_FRAMES = 50
9
+ MAX_FRAME_STACK = 10
10
+ FRAME_DECAY = 0.02
11
+ FRAME_STRENGTH_FLOOR = 0.05
12
+ SWITCH_COST = 0.15
13
+ SWITCH_COOLDOWN = 5
14
+ FAMILIARITY_ALPHA = 0.12
15
+ DEFAULT_FAMILIARITY = 0.3
16
+ RELEVANCE_THRESHOLD = 0.2
17
+ MAX_CUES_PER_FRAME = 30
18
+ MAX_HISTORY = 200
19
+
20
+ FRAME_LABELS = {
21
+ (0.8..) => :dominant,
22
+ (0.5...0.8) => :active,
23
+ (0.2...0.5) => :background,
24
+ (..0.2) => :fading
25
+ }.freeze
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Context
6
+ module Helpers
7
+ class ContextManager
8
+ include Constants
9
+
10
+ attr_reader :frames, :active_stack, :switch_history
11
+
12
+ def initialize
13
+ @frames = []
14
+ @active_stack = []
15
+ @switch_history = []
16
+ @last_switch_at = nil
17
+ end
18
+
19
+ def create_frame(name:, domain: :general, cues: [])
20
+ frame = Frame.new(name: name, domain: domain, cues: cues)
21
+ @frames << frame
22
+ trim_frames
23
+ frame
24
+ end
25
+
26
+ def activate(frame_id)
27
+ frame = find(frame_id)
28
+ return nil unless frame
29
+
30
+ previous = current_frame
31
+ cost = 0.0
32
+
33
+ if previous && previous.id != frame_id
34
+ cost = compute_switch_cost(previous, frame)
35
+ previous.deactivate
36
+ record_switch(from: previous, to: frame, cost: cost)
37
+ end
38
+
39
+ @active_stack.reject! { |f| f.id == frame_id }
40
+ @active_stack.push(frame)
41
+ @active_stack.shift while @active_stack.size > MAX_FRAME_STACK
42
+ frame.activate
43
+ { frame: frame, switch_cost: cost }
44
+ end
45
+
46
+ def current_frame
47
+ @active_stack.last
48
+ end
49
+
50
+ def detect_context(input_cues)
51
+ scored = @frames.map { |f| [f, f.match_score(input_cues)] }
52
+ scored.select! { |_, score| score >= RELEVANCE_THRESHOLD }
53
+ scored.sort_by! { |_, score| -score }
54
+ scored.map { |f, score| { frame: f, relevance: score } }
55
+ end
56
+
57
+ def auto_switch(input_cues)
58
+ candidates = detect_context(input_cues)
59
+ return nil if candidates.empty?
60
+
61
+ best = candidates.first
62
+ current = current_frame
63
+
64
+ return nil if current && current.id == best[:frame].id
65
+
66
+ activate(best[:frame].id)
67
+ end
68
+
69
+ def find(frame_id)
70
+ @frames.find { |f| f.id == frame_id }
71
+ end
72
+
73
+ def find_by_name(name)
74
+ @frames.select { |f| f.name == name }
75
+ end
76
+
77
+ def in_domain(domain)
78
+ @frames.select { |f| f.domain == domain }
79
+ end
80
+
81
+ def decay_all
82
+ @frames.each(&:decay)
83
+ @frames.reject!(&:stale?)
84
+ @active_stack.reject!(&:stale?)
85
+ end
86
+
87
+ def remove(frame_id)
88
+ @frames.reject! { |f| f.id == frame_id }
89
+ @active_stack.reject! { |f| f.id == frame_id }
90
+ end
91
+
92
+ def switch_cost_average
93
+ return 0.0 if @switch_history.empty?
94
+
95
+ @switch_history.sum { |s| s[:cost] }.to_f / @switch_history.size
96
+ end
97
+
98
+ def to_h
99
+ {
100
+ frame_count: @frames.size,
101
+ active_frame: current_frame&.name,
102
+ stack_depth: @active_stack.size,
103
+ switch_count: @switch_history.size,
104
+ avg_switch_cost: switch_cost_average.round(4),
105
+ by_domain: @frames.group_by(&:domain).transform_values(&:size)
106
+ }
107
+ end
108
+
109
+ private
110
+
111
+ def compute_switch_cost(from_frame, to_frame)
112
+ base = SWITCH_COST
113
+ familiarity_discount = to_frame.familiarity * 0.2
114
+ domain_bonus = from_frame.domain == to_frame.domain ? 0.02 : 0.0
115
+ cooldown_penalty = in_cooldown? ? 0.1 : 0.0
116
+ (base - familiarity_discount - domain_bonus + cooldown_penalty).clamp(0.0, 1.0)
117
+ end
118
+
119
+ def in_cooldown?
120
+ return false unless @last_switch_at
121
+
122
+ (Time.now.utc - @last_switch_at) < SWITCH_COOLDOWN
123
+ end
124
+
125
+ def record_switch(from:, to:, cost:)
126
+ @last_switch_at = Time.now.utc
127
+ @switch_history << {
128
+ from: from.name,
129
+ to: to.name,
130
+ cost: cost,
131
+ at: @last_switch_at
132
+ }
133
+ @switch_history.shift while @switch_history.size > MAX_HISTORY
134
+ end
135
+
136
+ def trim_frames
137
+ return unless @frames.size > MAX_FRAMES
138
+
139
+ @frames.sort_by!(&:strength)
140
+ @frames.shift(@frames.size - MAX_FRAMES)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Context
8
+ module Helpers
9
+ class Frame
10
+ include Constants
11
+
12
+ attr_reader :id, :name, :domain, :cues, :strength, :familiarity,
13
+ :created_at, :last_activated, :activation_count
14
+
15
+ def initialize(name:, domain: :general, cues: [])
16
+ @id = SecureRandom.uuid
17
+ @name = name
18
+ @domain = domain
19
+ @cues = cues.first(MAX_CUES_PER_FRAME)
20
+ @strength = 1.0
21
+ @familiarity = DEFAULT_FAMILIARITY
22
+ @created_at = Time.now.utc
23
+ @last_activated = @created_at
24
+ @activation_count = 0
25
+ end
26
+
27
+ def activate
28
+ @last_activated = Time.now.utc
29
+ @activation_count += 1
30
+ @strength = [@strength + 0.1, 1.0].min
31
+ update_familiarity(1.0)
32
+ end
33
+
34
+ def deactivate
35
+ update_familiarity(0.0)
36
+ end
37
+
38
+ def decay
39
+ @strength = [(@strength - FRAME_DECAY), 0.0].max
40
+ end
41
+
42
+ def match_score(input_cues)
43
+ return 0.0 if @cues.empty? || input_cues.empty?
44
+
45
+ overlap = (@cues & input_cues).size
46
+ overlap.to_f / @cues.size
47
+ end
48
+
49
+ def add_cue(cue)
50
+ return if @cues.include?(cue)
51
+
52
+ @cues << cue
53
+ @cues.shift if @cues.size > MAX_CUES_PER_FRAME
54
+ end
55
+
56
+ def remove_cue(cue)
57
+ @cues.delete(cue)
58
+ end
59
+
60
+ def label
61
+ FRAME_LABELS.each do |range, lbl|
62
+ return lbl if range.cover?(@strength)
63
+ end
64
+ :fading
65
+ end
66
+
67
+ def stale?
68
+ @strength < FRAME_STRENGTH_FLOOR
69
+ end
70
+
71
+ def to_h
72
+ {
73
+ id: @id,
74
+ name: @name,
75
+ domain: @domain,
76
+ strength: @strength,
77
+ familiarity: @familiarity,
78
+ label: label,
79
+ cues: @cues.dup,
80
+ activation_count: @activation_count,
81
+ created_at: @created_at,
82
+ last_activated: @last_activated
83
+ }
84
+ end
85
+
86
+ private
87
+
88
+ def update_familiarity(signal)
89
+ @familiarity = ((FAMILIARITY_ALPHA * signal) + ((1.0 - FAMILIARITY_ALPHA) * @familiarity)).clamp(0.0, 1.0)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Context
6
+ module Runners
7
+ module Context
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_context(name:, domain: :general, cues: [], **)
12
+ frame = context_manager.create_frame(name: name, domain: domain, cues: cues)
13
+ Legion::Logging.debug "[context] created frame=#{name} domain=#{domain} cues=#{cues.size}"
14
+ { success: true, frame: frame.to_h }
15
+ end
16
+
17
+ def activate_context(frame_id:, **)
18
+ result = context_manager.activate(frame_id)
19
+ return { success: false, reason: :not_found } unless result
20
+
21
+ frame = result[:frame]
22
+ Legion::Logging.debug "[context] activated frame=#{frame.name} cost=#{result[:switch_cost].round(3)}"
23
+ { success: true, frame: frame.to_h, switch_cost: result[:switch_cost] }
24
+ end
25
+
26
+ def detect_context(input_cues:, **)
27
+ matches = context_manager.detect_context(input_cues)
28
+ Legion::Logging.debug "[context] detect: #{matches.size} candidates for #{input_cues.size} cues"
29
+ {
30
+ success: true,
31
+ candidates: matches.map { |m| { frame: m[:frame].to_h, relevance: m[:relevance] } },
32
+ count: matches.size,
33
+ best: matches.first && { name: matches.first[:frame].name, relevance: matches.first[:relevance] }
34
+ }
35
+ end
36
+
37
+ def auto_switch(input_cues:, **)
38
+ result = context_manager.auto_switch(input_cues)
39
+ return { success: true, switched: false, reason: :no_better_match } unless result
40
+
41
+ frame = result[:frame]
42
+ Legion::Logging.debug "[context] auto-switched to frame=#{frame.name} cost=#{result[:switch_cost].round(3)}"
43
+ { success: true, switched: true, frame: frame.to_h, switch_cost: result[:switch_cost] }
44
+ end
45
+
46
+ def current_context(**)
47
+ frame = context_manager.current_frame
48
+ return { success: true, frame: nil } unless frame
49
+
50
+ { success: true, frame: frame.to_h }
51
+ end
52
+
53
+ def update_context(**)
54
+ context_manager.decay_all
55
+ Legion::Logging.debug "[context] tick: frames=#{context_manager.frames.size} active=#{context_manager.current_frame&.name}"
56
+ { success: true, frame_count: context_manager.frames.size, active: context_manager.current_frame&.name }
57
+ end
58
+
59
+ def add_cue(frame_id:, cue:, **)
60
+ frame = context_manager.find(frame_id)
61
+ return { success: false, reason: :not_found } unless frame
62
+
63
+ frame.add_cue(cue)
64
+ { success: true, frame: frame.to_h }
65
+ end
66
+
67
+ def frames_in_domain(domain:, **)
68
+ frames = context_manager.in_domain(domain)
69
+ { success: true, frames: frames.map(&:to_h), count: frames.size }
70
+ end
71
+
72
+ def remove_context(frame_id:, **)
73
+ context_manager.remove(frame_id)
74
+ { success: true }
75
+ end
76
+
77
+ def context_stats(**)
78
+ { success: true, stats: context_manager.to_h }
79
+ end
80
+
81
+ private
82
+
83
+ def context_manager
84
+ @context_manager ||= Helpers::ContextManager.new
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Context
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/context/version'
4
+ require 'legion/extensions/context/helpers/constants'
5
+ require 'legion/extensions/context/helpers/frame'
6
+ require 'legion/extensions/context/helpers/context_manager'
7
+ require 'legion/extensions/context/runners/context'
8
+ require 'legion/extensions/context/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Context
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-context
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: Models situated conceptualization — maintains context frames that shape
27
+ perception, memory retrieval, and action selection. Tracks context switches with
28
+ measurable switch costs and familiarity-based discount.
29
+ email:
30
+ - matt@iverson.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - lib/legion/extensions/context.rb
37
+ - lib/legion/extensions/context/client.rb
38
+ - lib/legion/extensions/context/helpers/constants.rb
39
+ - lib/legion/extensions/context/helpers/context_manager.rb
40
+ - lib/legion/extensions/context/helpers/frame.rb
41
+ - lib/legion/extensions/context/runners/context.rb
42
+ - lib/legion/extensions/context/version.rb
43
+ homepage: https://github.com/LegionIO/lex-context
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ rubygems_mfa_required: 'true'
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.4'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.6.9
63
+ specification_version: 4
64
+ summary: Contextual framing and situation model for LegionIO
65
+ test_files: []