lex-identity 0.2.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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Runners
7
+ module Identity
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def observe_behavior(dimension:, value:, **)
12
+ fingerprint = identity_fingerprint
13
+ fingerprint.observe(dimension, value)
14
+
15
+ Legion::Logging.debug "[identity] observe: dim=#{dimension} val=#{value.round(2)} " \
16
+ "obs=#{fingerprint.observation_count} maturity=#{fingerprint.maturity}"
17
+ {
18
+ dimension: dimension,
19
+ recorded: true,
20
+ observation_count: fingerprint.observation_count,
21
+ maturity: fingerprint.maturity
22
+ }
23
+ end
24
+
25
+ def observe_all(observations:, **)
26
+ fingerprint = identity_fingerprint
27
+ fingerprint.observe_all(observations)
28
+
29
+ Legion::Logging.debug "[identity] observe_all: dims=#{observations.keys.join(',')} " \
30
+ "obs=#{fingerprint.observation_count} maturity=#{fingerprint.maturity}"
31
+ {
32
+ dimensions_observed: observations.keys,
33
+ observation_count: fingerprint.observation_count,
34
+ maturity: fingerprint.maturity
35
+ }
36
+ end
37
+
38
+ def check_entropy(observations: {}, **)
39
+ fingerprint = identity_fingerprint
40
+ entropy = fingerprint.current_entropy(observations)
41
+ classification = Helpers::Dimensions.classify_entropy(entropy)
42
+ trend = fingerprint.entropy_trend
43
+
44
+ result = {
45
+ entropy: entropy,
46
+ classification: classification,
47
+ trend: trend,
48
+ in_range: Helpers::Dimensions::OPTIMAL_ENTROPY_RANGE.cover?(entropy)
49
+ }
50
+
51
+ case classification
52
+ when :high_entropy
53
+ result[:warning] = :possible_impersonation_or_drift
54
+ result[:action] = :enter_caution_mode
55
+ Legion::Logging.warn "[identity] high entropy detected: #{entropy.round(3)} trend=#{trend} - possible impersonation"
56
+ when :low_entropy
57
+ result[:warning] = :possible_automation
58
+ result[:action] = :trigger_verification
59
+ Legion::Logging.warn "[identity] low entropy detected: #{entropy.round(3)} trend=#{trend} - possible automation"
60
+ else
61
+ Legion::Logging.debug "[identity] entropy check: #{entropy.round(3)} classification=#{classification} trend=#{trend}"
62
+ end
63
+
64
+ result
65
+ end
66
+
67
+ def identity_status(**)
68
+ fingerprint = identity_fingerprint
69
+ Legion::Logging.debug "[identity] status: maturity=#{fingerprint.maturity} observations=#{fingerprint.observation_count}"
70
+ fingerprint.to_h
71
+ end
72
+
73
+ def identity_maturity(**)
74
+ { maturity: identity_fingerprint.maturity }
75
+ end
76
+
77
+ private
78
+
79
+ def identity_fingerprint
80
+ @identity_fingerprint ||= Helpers::Fingerprint.new
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ VERSION = '0.2.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/identity/version'
4
+ require 'legion/extensions/identity/helpers/dimensions'
5
+ require 'legion/extensions/identity/helpers/fingerprint'
6
+ require 'legion/extensions/identity/helpers/vault_secrets'
7
+ require 'legion/extensions/identity/runners/identity'
8
+ require 'legion/extensions/identity/runners/entra'
9
+ require 'legion/extensions/identity/actors/orphan_check'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Identity
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
18
+
19
+ if defined?(Legion::Data::Local)
20
+ Legion::Data::Local.register_migrations(
21
+ name: :identity,
22
+ path: File.join(__dir__, 'identity', 'local_migrations')
23
+ )
24
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ module Legion
5
+ module Extensions
6
+ module Actors
7
+ class Every # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ # Intercept the require in the actor file so it doesn't fail
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
15
+
16
+ require 'legion/extensions/identity/actors/orphan_check'
17
+
18
+ RSpec.describe Legion::Extensions::Identity::Actor::OrphanCheck do
19
+ subject(:actor) { described_class.new }
20
+
21
+ describe 'ORPHAN_CHECK_INTERVAL' do
22
+ it 'is 14400 seconds (4 hours)' do
23
+ expect(described_class::ORPHAN_CHECK_INTERVAL).to eq(14_400)
24
+ end
25
+ end
26
+
27
+ describe '#runner_class' do
28
+ it 'returns the Entra module' do
29
+ expect(actor.runner_class).to eq(Legion::Extensions::Identity::Runners::Entra)
30
+ end
31
+ end
32
+
33
+ describe '#runner_function' do
34
+ it 'returns check_orphans' do
35
+ expect(actor.runner_function).to eq('check_orphans')
36
+ end
37
+ end
38
+
39
+ describe '#time' do
40
+ it 'returns 14400 seconds using the constant' do
41
+ expect(actor.time).to eq(Legion::Extensions::Identity::Actor::OrphanCheck::ORPHAN_CHECK_INTERVAL)
42
+ end
43
+
44
+ it 'returns 14400' do
45
+ expect(actor.time).to eq(14_400)
46
+ end
47
+ end
48
+
49
+ describe '#use_runner?' do
50
+ it 'returns false' do
51
+ expect(actor.use_runner?).to be false
52
+ end
53
+ end
54
+
55
+ describe '#check_subtask?' do
56
+ it 'returns false' do
57
+ expect(actor.check_subtask?).to be false
58
+ end
59
+ end
60
+
61
+ describe '#generate_task?' do
62
+ it 'returns false' do
63
+ expect(actor.generate_task?).to be false
64
+ end
65
+ end
66
+
67
+ describe '#enabled?' do
68
+ context 'when Legion::Data is not defined' do
69
+ it 'returns false' do
70
+ hide_const('Legion::Data') if defined?(Legion::Data)
71
+ expect(actor.enabled?).to be_falsey
72
+ end
73
+ end
74
+
75
+ context 'when Legion::Data is defined and data is connected' do
76
+ it 'returns truthy' do
77
+ stub_const('Legion::Data', Module.new)
78
+ stub_const('Legion::Settings', Class.new)
79
+ settings_double = { connected: true }
80
+ allow(Legion::Settings).to receive(:[]).with(:data).and_return(settings_double)
81
+ expect(actor.enabled?).to be_truthy
82
+ end
83
+ end
84
+
85
+ context 'when Legion::Data is defined but connected is false' do
86
+ it 'returns false' do
87
+ stub_const('Legion::Data', Module.new)
88
+ stub_const('Legion::Settings', Class.new)
89
+ settings_double = { connected: false }
90
+ allow(Legion::Settings).to receive(:[]).with(:data).and_return(settings_double)
91
+ expect(actor.enabled?).to be false
92
+ end
93
+ end
94
+
95
+ context 'when Legion::Settings raises an error' do
96
+ it 'returns false' do
97
+ stub_const('Legion::Data', Module.new)
98
+ stub_const('Legion::Settings', Class.new)
99
+ allow(Legion::Settings).to receive(:[]).with(:data).and_raise(StandardError)
100
+ expect(actor.enabled?).to be false
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/identity/client'
4
+
5
+ RSpec.describe Legion::Extensions::Identity::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to identity runner methods' do
9
+ expect(client).to respond_to(:observe_behavior)
10
+ expect(client).to respond_to(:observe_all)
11
+ expect(client).to respond_to(:check_entropy)
12
+ expect(client).to respond_to(:identity_status)
13
+ expect(client).to respond_to(:identity_maturity)
14
+ end
15
+
16
+ it 'round-trips identity lifecycle' do
17
+ # Build identity
18
+ 50.times do
19
+ client.observe_all(observations: {
20
+ communication_cadence: 0.5 + (rand * 0.1),
21
+ vocabulary_patterns: 0.6 + (rand * 0.1),
22
+ emotional_response: 0.4 + (rand * 0.1)
23
+ })
24
+ end
25
+
26
+ expect(client.identity_maturity[:maturity]).to eq(:established)
27
+
28
+ # Check entropy with consistent behavior
29
+ result = client.check_entropy(observations: { communication_cadence: 0.55 })
30
+ expect(result[:entropy]).to be_between(0.0, 1.0)
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Identity::Helpers::Dimensions do
4
+ describe '.new_identity_model' do
5
+ it 'creates a model with 6 dimensions' do
6
+ model = described_class.new_identity_model
7
+ expect(model.size).to eq(6)
8
+ described_class::IDENTITY_DIMENSIONS.each do |dim|
9
+ expect(model[dim][:mean]).to eq(0.5)
10
+ end
11
+ end
12
+ end
13
+
14
+ describe '.compute_entropy' do
15
+ it 'returns 0.5 for empty observations' do
16
+ model = described_class.new_identity_model
17
+ expect(described_class.compute_entropy({}, model)).to eq(0.5)
18
+ end
19
+
20
+ it 'returns low entropy for observations matching baseline' do
21
+ model = described_class.new_identity_model
22
+ model[:communication_cadence][:observations] = 50
23
+ obs = { communication_cadence: 0.5 }
24
+ entropy = described_class.compute_entropy(obs, model)
25
+ expect(entropy).to be < 0.3
26
+ end
27
+
28
+ it 'returns high entropy for observations diverging from baseline' do
29
+ model = described_class.new_identity_model
30
+ model[:communication_cadence][:observations] = 50
31
+ model[:communication_cadence][:variance] = 0.1
32
+ obs = { communication_cadence: 1.0 }
33
+ entropy = described_class.compute_entropy(obs, model)
34
+ expect(entropy).to be > 0.3
35
+ end
36
+ end
37
+
38
+ describe '.classify_entropy' do
39
+ it 'classifies high entropy' do
40
+ expect(described_class.classify_entropy(0.8)).to eq(:high_entropy)
41
+ end
42
+
43
+ it 'classifies low entropy' do
44
+ expect(described_class.classify_entropy(0.1)).to eq(:low_entropy)
45
+ end
46
+
47
+ it 'classifies normal entropy' do
48
+ expect(described_class.classify_entropy(0.5)).to eq(:normal)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Identity::Helpers::Fingerprint do
4
+ let(:fp) { described_class.new }
5
+
6
+ describe '#observe' do
7
+ it 'records observation for valid dimension' do
8
+ fp.observe(:communication_cadence, 0.7)
9
+ expect(fp.observation_count).to eq(1)
10
+ end
11
+
12
+ it 'ignores invalid dimensions' do
13
+ fp.observe(:nonexistent, 0.5)
14
+ expect(fp.observation_count).to eq(0)
15
+ end
16
+
17
+ it 'shifts mean toward observed values' do
18
+ original = fp.model[:vocabulary_patterns][:mean]
19
+ 10.times { fp.observe(:vocabulary_patterns, 0.9) }
20
+ expect(fp.model[:vocabulary_patterns][:mean]).to be > original
21
+ end
22
+ end
23
+
24
+ describe '#observe_all' do
25
+ it 'records multiple dimensions at once' do
26
+ fp.observe_all(communication_cadence: 0.6, vocabulary_patterns: 0.7)
27
+ expect(fp.observation_count).to eq(2)
28
+ end
29
+ end
30
+
31
+ describe '#current_entropy' do
32
+ it 'computes entropy against model' do
33
+ entropy = fp.current_entropy(communication_cadence: 0.5)
34
+ expect(entropy).to be_between(0.0, 1.0)
35
+ end
36
+
37
+ it 'tracks entropy history' do
38
+ 3.times { fp.current_entropy(communication_cadence: 0.5) }
39
+ expect(fp.entropy_history.size).to eq(3)
40
+ end
41
+ end
42
+
43
+ describe '#entropy_trend' do
44
+ it 'returns stable for insufficient data' do
45
+ expect(fp.entropy_trend).to eq(:stable)
46
+ end
47
+
48
+ it 'detects rising entropy' do
49
+ 5.times { |i| fp.current_entropy(communication_cadence: 0.5 + (i * 0.1)) }
50
+ # Trend depends on actual computed values
51
+ trend = fp.entropy_trend(window: 5)
52
+ expect(%i[rising stable falling]).to include(trend)
53
+ end
54
+ end
55
+
56
+ describe '#maturity' do
57
+ it 'starts as nascent' do
58
+ expect(fp.maturity).to eq(:nascent)
59
+ end
60
+
61
+ it 'progresses to developing' do
62
+ 15.times { fp.observe(:communication_cadence, 0.5) }
63
+ expect(fp.maturity).to eq(:developing)
64
+ end
65
+ end
66
+ end