lex-agentic-social 0.1.15 → 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: 4ae718ace3eafb04ea8f811bb5cf0d9ef384e6b63887f1423c8461b04269a524
4
- data.tar.gz: 84e78ce21707ba78c4f90dc5168a99ca494fa4610d12cc609a72f5664cf43bee
3
+ metadata.gz: e415a1e8df317c84447b21607aa89a50091a52ee603c103532434e613b5c79f8
4
+ data.tar.gz: '0485cc912a12c05e87ed0d6a89f34a20e678f92bdb1fd33f40bee1f03fd5f6ce'
5
5
  SHA512:
6
- metadata.gz: 14f3884ae4814a76e9872081d132cd4b9b3fbf5a3dcf247541a67e5cf6c4d3a152e071753de8fc45a50a99cdb83df78dc2a8019f1fb949dea1f628a4da7870af
7
- data.tar.gz: 3f991289c88888c67ddb146e094ea07f39f7fd79698c35c1b8b4bbb01cf29c74e6b256238c4b0f6b2c411eab6a070893db5350a99c8c1f0e7775461eca2c0ce4
6
+ metadata.gz: 73b527381fc34d087abce74fd3fa0f704bd8c2717c5100b2b822a83c8a392c9a79aa5a44e78d9667d6429b733390d31056c1573661e03696ea317fdb25fecb30
7
+ data.tar.gz: 37e46998cc523737472ca6884db0a24320c18e252190cef94fefed44e70b70f5c60c57fa30f20832d7aed9cc847fb6d90615d1a7589b754e12099cadde1edbaf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## [0.1.15] - 2026-04-27
4
10
  ### Fixed
5
11
  - Stop social calibration partner-knowledge promotion when Legion is shutting down or Apollo Local becomes unavailable between promotable tag groups
@@ -194,14 +194,17 @@ module Legion
194
194
 
195
195
  base_tags = %w[partner preference llm_inference]
196
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
197
200
  content = Legion::JSON.dump({
198
- 'domain' => pref['domain'],
199
- 'value' => pref['value'],
201
+ 'domain' => domain,
202
+ 'value' => value,
200
203
  'source' => 'llm_inference',
201
- 'confidence' => pref['confidence'] || 0.65
204
+ 'confidence' => confidence
202
205
  })
203
- tags = base_tags + ["preference:#{pref['domain']}"]
204
- 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)
205
208
  end
206
209
  end
207
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.15'
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
@@ -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.15
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity