lex-agentic-social 0.1.14 → 0.1.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 785afcf396e852ec5c417d3ddd68f04dc62bb290f5687a9115156f98cf5f59d2
4
- data.tar.gz: f6ee2703b49a5e2dcc331596f703dee60a8ddd8ea601425439e26cc4b2e00706
3
+ metadata.gz: e415a1e8df317c84447b21607aa89a50091a52ee603c103532434e613b5c79f8
4
+ data.tar.gz: '0485cc912a12c05e87ed0d6a89f34a20e678f92bdb1fd33f40bee1f03fd5f6ce'
5
5
  SHA512:
6
- metadata.gz: 687b70c4adafc0718c5634a83ad86353bc02f3b42782eae1d62b637081a4b69f9e4554942f8e9b2f09b28d78d6f4571a09d422259e160c69056bdd091f06d5bd
7
- data.tar.gz: c6236731a1a0fae572ed1a36af0b7e7f8e842a95b64b1eac16331a2a366782d99a7313f82a7faa0f9f67b30668f25242a7a4ffd7573ede3105c6de1474b069e3
6
+ metadata.gz: 73b527381fc34d087abce74fd3fa0f704bd8c2717c5100b2b822a83c8a392c9a79aa5a44e78d9667d6429b733390d31056c1573661e03696ea317fdb25fecb30
7
+ data.tar.gz: 37e46998cc523737472ca6884db0a24320c18e252190cef94fefed44e70b70f5c60c57fa30f20832d7aed9cc847fb6d90615d1a7589b754e12099cadde1edbaf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.16] - 2026-05-07
4
+ ### Fixed
5
+ - Calibration preference storage now handles symbol-keyed parsed LLM preferences without dropping domain or value.
6
+ - Consent restore preserves pending approval fields after local DB reloads.
7
+ - Shadow AI scans flag scan failures as issues, conflict stale checks parse restored string timestamps, and moral reasoning reinforcement uses dilemma severity.
8
+
9
+ ## [0.1.15] - 2026-04-27
10
+ ### Fixed
11
+ - Stop social calibration partner-knowledge promotion when Legion is shutting down or Apollo Local becomes unavailable between promotable tag groups
12
+
3
13
  ## [0.1.14] - 2026-04-22
4
14
  ### Added
5
15
  - `Governance#review_transition` method — API expected by lex-extinction for containment governance gate
@@ -79,10 +79,14 @@ module Legion
79
79
  end
80
80
 
81
81
  def promote_partner_knowledge(**)
82
+ return { success: true, skipped: :shutting_down } if shutting_down?
82
83
  return { success: true, skipped: :local_unavailable } unless apollo_local_available?
83
84
 
84
85
  total = 0
85
86
  Helpers::Constants::PROMOTABLE_TAGS.each do |tags|
87
+ return { success: true, promoted: total, stopped: :shutting_down } if shutting_down?
88
+ return { success: true, promoted: total, stopped: :local_unavailable } unless apollo_local_available?
89
+
86
90
  result = Legion::Apollo::Local.promote_to_global(tags: tags, min_confidence: Helpers::Constants::PROMOTION_MIN_CONFIDENCE)
87
91
  total += result[:promoted] if result[:success]
88
92
  end
@@ -112,6 +116,14 @@ module Legion
112
116
  defined?(Legion::Apollo::Local) && Legion::Apollo::Local.started?
113
117
  end
114
118
 
119
+ def shutting_down?
120
+ return Legion.shutting_down? if defined?(Legion) && Legion.respond_to?(:shutting_down?)
121
+
122
+ defined?(Legion::Settings) && Legion::Settings.dig(:client, :shutting_down) == true
123
+ rescue StandardError => _e
124
+ false
125
+ end
126
+
115
127
  def retrieve_interaction_traces
116
128
  traces = retrieve_from_memory
117
129
  return traces if traces.any?
@@ -182,14 +194,17 @@ module Legion
182
194
 
183
195
  base_tags = %w[partner preference llm_inference]
184
196
  preferences.each do |pref|
197
+ domain = pref[:domain] || pref['domain']
198
+ value = pref[:value] || pref['value']
199
+ confidence = pref[:confidence] || pref['confidence'] || 0.65
185
200
  content = Legion::JSON.dump({
186
- 'domain' => pref['domain'],
187
- 'value' => pref['value'],
201
+ 'domain' => domain,
202
+ 'value' => value,
188
203
  'source' => 'llm_inference',
189
- 'confidence' => pref['confidence'] || 0.65
204
+ 'confidence' => confidence
190
205
  })
191
- tags = base_tags + ["preference:#{pref['domain']}"]
192
- Legion::Apollo::Local.upsert(content: content, tags: tags, confidence: pref['confidence'] || 0.65)
206
+ tags = base_tags + ["preference:#{domain}"]
207
+ Legion::Apollo::Local.upsert(content: content, tags: tags, confidence: confidence)
193
208
  end
194
209
  end
195
210
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Agentic
@@ -70,12 +72,12 @@ module Legion
70
72
 
71
73
  def check_stale_conflicts(**)
72
74
  active = conflict_log.active_conflicts
73
- stale = active.select { |c| Time.now.utc - c[:created_at] > Helpers::Severity::STALE_CONFLICT_TIMEOUT }
75
+ stale = active.select { |c| conflict_age_seconds(c) > Helpers::Severity::STALE_CONFLICT_TIMEOUT }
74
76
  stale.each do |c|
75
77
  message = 'conflict marked stale — no resolution after 24h'
76
78
 
77
79
  if Helpers::LlmEnhancer.available?
78
- age_hours = (Time.now.utc - c[:created_at]) / 3600.0
80
+ age_hours = conflict_age_seconds(c) / 3600.0
79
81
  analysis = Helpers::LlmEnhancer.analyze_stale_conflict(
80
82
  description: c[:description],
81
83
  severity: c[:severity],
@@ -103,6 +105,12 @@ module Legion
103
105
  def conflict_log
104
106
  @conflict_log ||= Helpers::ConflictLog.new
105
107
  end
108
+
109
+ def conflict_age_seconds(conflict)
110
+ created_at = conflict[:created_at]
111
+ created_at = Time.parse(created_at) if created_at.is_a?(String)
112
+ Time.now.utc - created_at
113
+ end
106
114
  end
107
115
  end
108
116
  end
@@ -170,14 +170,14 @@ module Legion
170
170
  []
171
171
  end
172
172
 
173
- @domains[key] = {
173
+ @domains[key].merge!(
174
174
  tier: row[:tier].to_sym,
175
175
  success_count: row[:success_count].to_i,
176
176
  failure_count: row[:failure_count].to_i,
177
177
  total_actions: row[:total_actions].to_i,
178
178
  last_changed_at: row[:last_changed_at],
179
179
  history: history
180
- }
180
+ )
181
181
  end
182
182
  rescue StandardError => e
183
183
  Legion::Logging.warn "[consent] load_from_local failed: #{e.message}"
@@ -14,7 +14,8 @@ module Legion
14
14
  unregistered = installed - registered
15
15
  { installed: installed.size, registered: registered.size, unregistered: unregistered }
16
16
  rescue StandardError => e
17
- { installed: 0, registered: 0, unregistered: [], error: e.message }
17
+ Legion::Logging.warn("[governance:shadow_ai] extension scan failed: #{e.message}")
18
+ { installed: 0, registered: 0, unregistered: [], scan_failed: true, error: e.message }
18
19
  end
19
20
 
20
21
  def check_llm_bypass_indicators(**)
@@ -47,7 +48,8 @@ module Legion
47
48
  bypass = check_llm_bypass_indicators
48
49
  compliance = check_airb_compliance
49
50
 
50
- has_issues = extensions[:unregistered]&.any? || bypass[:bypassed] || compliance[:non_compliant]&.any?
51
+ has_issues = extensions[:scan_failed] || extensions[:unregistered]&.any? ||
52
+ bypass[:bypassed] || compliance[:non_compliant]&.any?
51
53
  emit_shadow_event(extensions, bypass, compliance) if has_issues
52
54
 
53
55
  { extensions: extensions, bypass: bypass, compliance: compliance, issues_found: has_issues }
@@ -117,7 +117,7 @@ module Legion
117
117
  return { success: false, reason: :invalid_option } unless chosen
118
118
 
119
119
  dilemma.resolve(option_id: option_id, reasoning: reasoning, framework: framework)
120
- reinforce_chosen_foundations(chosen)
120
+ reinforce_chosen_foundations(chosen, severity: dilemma.severity)
121
121
  weaken_unchosen_foundations(dilemma.options, option_id)
122
122
  add_history(type: :resolution, dilemma_id: dilemma_id, option_id: option_id,
123
123
  framework: framework, severity: dilemma.severity)
@@ -210,9 +210,9 @@ module Legion
210
210
  end
211
211
  end
212
212
 
213
- def reinforce_chosen_foundations(chosen_option)
213
+ def reinforce_chosen_foundations(chosen_option, severity:)
214
214
  chosen_option.fetch(:foundations, []).each do |fid|
215
- @foundations[fid]&.reinforce(amount: chosen_option.fetch(:severity, 1.0))
215
+ @foundations[fid]&.reinforce(amount: severity)
216
216
  end
217
217
  end
218
218
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Social
7
- VERSION = '0.1.14'
7
+ VERSION = '0.1.16'
8
8
  end
9
9
  end
10
10
  end
@@ -70,6 +70,24 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
70
70
  result = client.extract_preferences_via_llm
71
71
  expect(result[:skipped]).to eq(:llm_unavailable)
72
72
  end
73
+
74
+ it 'stores symbol-keyed parsed preferences with domain and value intact' do
75
+ mock_local = double('apollo_local')
76
+ stub_const('Legion::Apollo', Module.new)
77
+ stub_const('Legion::Apollo::Local', mock_local)
78
+ allow(mock_local).to receive(:started?).and_return(true)
79
+ allow(mock_local).to receive(:upsert)
80
+
81
+ client.send(:store_llm_preferences, [{ domain: 'tone', value: 'concise', confidence: 0.8 }])
82
+
83
+ expect(mock_local).to have_received(:upsert) do |args|
84
+ content = Legion::JSON.load(args[:content])
85
+ expect(content[:domain]).to eq('tone')
86
+ expect(content[:value]).to eq('concise')
87
+ expect(args[:tags]).to include('preference:tone')
88
+ expect(args[:confidence]).to eq(0.8)
89
+ end
90
+ end
73
91
  end
74
92
 
75
93
  describe '#retrieve_from_memory' do
@@ -146,6 +164,32 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
146
164
  result = client.promote_partner_knowledge
147
165
  expect(result[:skipped]).to eq(:local_unavailable)
148
166
  end
167
+
168
+ it 'skips when Legion is shutting down' do
169
+ allow(Legion::Settings).to receive(:dig).with(:client, :shutting_down).and_return(true)
170
+
171
+ result = client.promote_partner_knowledge
172
+ expect(result[:skipped]).to eq(:shutting_down)
173
+ end
174
+
175
+ it 'stops when Apollo Local becomes unavailable between tag groups' do
176
+ mock_local = double('apollo_local')
177
+ stub_const('Legion::Apollo', Module.new)
178
+ stub_const('Legion::Apollo::Local', mock_local)
179
+ allow(Legion::Settings).to receive(:dig).with(:client, :shutting_down).and_return(false)
180
+ allow(mock_local).to receive(:started?).and_return(true, true, false)
181
+ expect(mock_local).to receive(:promote_to_global)
182
+ .with(
183
+ tags: Legion::Extensions::Agentic::Social::Calibration::Helpers::Constants::PROMOTABLE_TAGS.first,
184
+ min_confidence: Legion::Extensions::Agentic::Social::Calibration::Helpers::Constants::PROMOTION_MIN_CONFIDENCE
185
+ )
186
+ .once
187
+ .and_return({ success: true, promoted: 2 })
188
+
189
+ result = client.promote_partner_knowledge
190
+ expect(result[:promoted]).to eq(2)
191
+ expect(result[:stopped]).to eq(:local_unavailable)
192
+ end
149
193
  end
150
194
 
151
195
  describe '#sync_partner_knowledge' do
@@ -78,6 +78,17 @@ RSpec.describe Legion::Extensions::Agentic::Social::Conflict::Runners::Conflict
78
78
  expect(result[:stale_ids]).to include(c[:conflict_id])
79
79
  end
80
80
 
81
+ it 'detects stale conflicts restored with string created_at values' do
82
+ c = client.register_conflict(parties: %w[a b], severity: :medium, description: 'old')
83
+ conflict = client.instance_variable_get(:@conflict_log).conflicts[c[:conflict_id]]
84
+ conflict[:created_at] = (Time.now.utc - (Legion::Extensions::Agentic::Social::Conflict::Helpers::Severity::STALE_CONFLICT_TIMEOUT + 1)).iso8601
85
+
86
+ result = client.check_stale_conflicts
87
+
88
+ expect(result[:stale_count]).to eq(1)
89
+ expect(result[:stale_ids]).to include(c[:conflict_id])
90
+ end
91
+
81
92
  it 'does not include resolved conflicts in stale check' do
82
93
  c = client.register_conflict(parties: %w[a b], severity: :low, description: 'resolved')
83
94
  conflict = client.instance_variable_get(:@conflict_log).conflicts[c[:conflict_id]]
@@ -122,6 +122,25 @@ RSpec.describe 'lex-consent local SQLite persistence' do
122
122
  expect(map.domains['email'][:total_actions]).to eq(6)
123
123
  end
124
124
 
125
+ it 'restores pending fields with default nil values' do
126
+ db[:consent_domains].insert(
127
+ domain_key: 'email',
128
+ tier: 'consult',
129
+ success_count: 1,
130
+ failure_count: 0,
131
+ total_actions: 1,
132
+ history: '[]'
133
+ )
134
+
135
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
136
+ expect(map.pending?('email')).to be false
137
+ expect(map.domains['email']).to include(
138
+ pending_tier: nil,
139
+ pending_since: nil,
140
+ pending_requested_by: nil
141
+ )
142
+ end
143
+
125
144
  it 'restores history as an array of hashes with symbol keys' do
126
145
  history_json = JSON.generate([{ 'from' => 'consult', 'to' => 'act_notify', 'at' => Time.now.utc.to_s }])
127
146
  db[:consent_domains].insert(
@@ -75,5 +75,20 @@ RSpec.describe Legion::Extensions::Agentic::Social::Governance::Runners::ShadowA
75
75
  expect(result[:issues_found]).to be_falsey
76
76
  expect(result[:extensions][:installed]).to eq(5)
77
77
  end
78
+
79
+ it 'treats extension scan failure as an issue' do
80
+ allow(host).to receive(:scan_unregistered_extensions).and_return(
81
+ { installed: 0, registered: 0, unregistered: [], scan_failed: true, error: 'Gemfile not found' }
82
+ )
83
+ allow(host).to receive(:check_llm_bypass_indicators).and_return(
84
+ { indicators: [], bypassed: false }
85
+ )
86
+ allow(host).to receive(:check_airb_compliance).and_return(
87
+ { checked: 0, source: :unavailable }
88
+ )
89
+
90
+ result = host.full_scan
91
+ expect(result[:issues_found]).to be true
92
+ end
78
93
  end
79
94
  end
@@ -89,6 +89,25 @@ RSpec.describe Legion::Extensions::Agentic::Social::MoralReasoning::Helpers::Mor
89
89
  expect(result[:dilemma][:resolved]).to be true
90
90
  end
91
91
 
92
+ it 'uses the dilemma severity when reinforcing chosen foundations' do
93
+ dilemma_id = engine.pose_dilemma(
94
+ description: 'Low severity',
95
+ options: options,
96
+ severity: 0.2
97
+ )[:dilemma][:id]
98
+ before_weight = engine.foundation_profile[:care][:weight]
99
+
100
+ engine.resolve_dilemma(
101
+ dilemma_id: dilemma_id,
102
+ option_id: 'opt_a',
103
+ reasoning: 'Least harm',
104
+ framework: :care_ethics
105
+ )
106
+
107
+ after_weight = engine.foundation_profile[:care][:weight]
108
+ expect(after_weight - before_weight).to be_within(0.001).of(0.02)
109
+ end
110
+
92
111
  it 'returns failure for unknown dilemma_id' do
93
112
  result = engine.resolve_dilemma(
94
113
  dilemma_id: 'nonexistent', option_id: 'opt_a',
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-social
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity