lex-cognitive-fingerprint 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: 1e72365652af428c7183e63627b14ab4f0d67519db773b2569cb61c557c2dac5
4
+ data.tar.gz: 5cca9e06f5d51d970c4c814bc17ab8e4aef1edb678b270c3bc8db4ac208d52d0
5
+ SHA512:
6
+ metadata.gz: 72c9955b658f540ff1907f381dc383acb956cba28166df7b69303a6a130638466d8f6fd1917eb2e965eaf854c8504c675a74eeaed99804193355225c0a05de20
7
+ data.tar.gz: 2085190392b9539481d25eaff325525fa68aa6bb27253af185f987eb5989797ac3942df9204d6e0c4c4c5ef74d0c9f72f7699c72d0c734d0cef64165e30dda11
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rspec_junit_formatter'
10
+ gem 'rubocop', '~> 1.75', require: false
11
+ gem 'rubocop-rspec', require: false
12
+ gem 'simplecov'
13
+ end
14
+
15
+ gem 'legion-gaia', path: '../../legion-gaia'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Iverson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # lex-cognitive-fingerprint
2
+
3
+ Behavioral fingerprinting and identity verification for cognitive entities in the LegionIO ecosystem.
4
+
5
+ ## What It Does
6
+
7
+ Builds a stable behavioral fingerprint by recording observations across eight trait dimensions: processing_speed, accuracy, creativity, caution, thoroughness, risk_tolerance, abstraction_preference, and social_orientation. Each observation updates a per-trait Exponential Moving Average baseline. Once a profile is established, identity verification compares a new set of observations against the fingerprint and returns a match score and verdict. Single-observation anomaly detection flags deviations that exceed the deviation threshold.
8
+
9
+ The fingerprint hash is a 16-character SHA256 derived from all trait baselines, providing a compact comparison key.
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require 'legion/extensions/cognitive_fingerprint'
15
+
16
+ client = Legion::Extensions::CognitiveFingerprint::Client.new
17
+
18
+ # Build a fingerprint by recording observations
19
+ client.record_observation(category: :accuracy, value: 0.85)
20
+ client.record_observation(category: :caution, value: 0.72)
21
+ client.record_observation(category: :creativity, value: 0.60)
22
+
23
+ # Check identity confidence once enough observations are recorded
24
+ client.identity_confidence
25
+ # => { confidence: 0.24, label: :uncertain }
26
+
27
+ # Verify identity against a set of new observations
28
+ client.verify_identity(observations: [
29
+ { category: :accuracy, value: 0.83 },
30
+ { category: :caution, value: 0.75 }
31
+ ])
32
+ # => { match_score: 0.93, verdict: :verified, observations_checked: 2 }
33
+
34
+ # Check single-observation anomaly
35
+ client.anomaly_check(category: :accuracy, value: 0.1)
36
+ # => { anomaly: true, deviation: 0.75, threshold: 0.3, ... }
37
+ ```
38
+
39
+ ## Development
40
+
41
+ ```bash
42
+ bundle install
43
+ bundle exec rspec
44
+ bundle exec rubocop
45
+ ```
46
+
47
+ ## License
48
+
49
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_fingerprint/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-fingerprint'
7
+ spec.version = Legion::Extensions::CognitiveFingerprint::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Fingerprint'
12
+ spec.description = 'Unique cognitive identity tracking via emergent trait patterns for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-fingerprint'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-fingerprint'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-fingerprint'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-fingerprint'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-fingerprint/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-fingerprint.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_fingerprint/helpers/constants'
4
+ require 'legion/extensions/cognitive_fingerprint/helpers/cognitive_trait'
5
+ require 'legion/extensions/cognitive_fingerprint/helpers/fingerprint_engine'
6
+ require 'legion/extensions/cognitive_fingerprint/runners/cognitive_fingerprint'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module CognitiveFingerprint
11
+ class Client
12
+ include Runners::CognitiveFingerprint
13
+
14
+ def initialize(**)
15
+ @fingerprint_engine = Helpers::FingerprintEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :fingerprint_engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveFingerprint
8
+ module Helpers
9
+ class CognitiveTrait
10
+ attr_reader :id, :category, :baseline, :variance, :sample_count, :last_updated
11
+
12
+ def initialize(category:)
13
+ raise ArgumentError, "unknown category: #{category}" unless Constants::TRAIT_CATEGORIES.include?(category)
14
+
15
+ @id = SecureRandom.uuid
16
+ @category = category
17
+ @baseline = 0.5
18
+ @variance = 0.0
19
+ @sample_count = 0
20
+ @last_updated = Time.now.utc
21
+ end
22
+
23
+ def record_sample!(value)
24
+ clamped = value.clamp(0.0, 1.0)
25
+ old_baseline = @baseline
26
+ @baseline = ((Constants::EMA_ALPHA * clamped) + ((1.0 - Constants::EMA_ALPHA) * old_baseline)).round(10)
27
+
28
+ deviation = (clamped - @baseline).abs
29
+ @variance = ((Constants::EMA_ALPHA * deviation) + ((1.0 - Constants::EMA_ALPHA) * @variance)).round(10)
30
+
31
+ @sample_count = [@sample_count + 1, Constants::MAX_SAMPLES].min
32
+ @last_updated = Time.now.utc
33
+ self
34
+ end
35
+
36
+ def deviation_from(value)
37
+ (value.clamp(0.0, 1.0) - @baseline).abs.round(10)
38
+ end
39
+
40
+ def stable?
41
+ @variance <= 0.1
42
+ end
43
+
44
+ def volatile?
45
+ @variance >= 0.3
46
+ end
47
+
48
+ def strength_label
49
+ Constants.trait_strength_label_for(@baseline)
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ id: @id,
55
+ category: @category,
56
+ baseline: @baseline,
57
+ variance: @variance,
58
+ sample_count: @sample_count,
59
+ stable: stable?,
60
+ volatile: volatile?,
61
+ strength: strength_label,
62
+ last_updated: @last_updated
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFingerprint
6
+ module Helpers
7
+ module Constants
8
+ MAX_TRAITS = 100
9
+ MAX_SAMPLES = 500
10
+ EMA_ALPHA = 0.15
11
+
12
+ TRAIT_CATEGORIES = %i[
13
+ processing_speed
14
+ accuracy
15
+ creativity
16
+ caution
17
+ thoroughness
18
+ risk_tolerance
19
+ abstraction_preference
20
+ social_orientation
21
+ ].freeze
22
+
23
+ DEVIATION_THRESHOLD = 0.3
24
+
25
+ IDENTITY_CONFIDENCE_LABELS = [
26
+ { range: (0.85..1.0), label: :certain },
27
+ { range: (0.65...0.85), label: :confident },
28
+ { range: (0.40...0.65), label: :developing },
29
+ { range: (0.20...0.40), label: :uncertain },
30
+ { range: (0.0...0.20), label: :unknown }
31
+ ].freeze
32
+
33
+ TRAIT_STRENGTH_LABELS = [
34
+ { range: (0.80..1.0), label: :dominant },
35
+ { range: (0.60...0.80), label: :strong },
36
+ { range: (0.40...0.60), label: :moderate },
37
+ { range: (0.20...0.40), label: :weak },
38
+ { range: (0.0...0.20), label: :absent }
39
+ ].freeze
40
+
41
+ module_function
42
+
43
+ def identity_label_for(confidence)
44
+ entry = IDENTITY_CONFIDENCE_LABELS.find { |e| e[:range].cover?(confidence) }
45
+ entry ? entry[:label] : :unknown
46
+ end
47
+
48
+ def trait_strength_label_for(value)
49
+ entry = TRAIT_STRENGTH_LABELS.find { |e| e[:range].cover?(value) }
50
+ entry ? entry[:label] : :absent
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFingerprint
6
+ module Helpers
7
+ class FingerprintEngine
8
+ attr_reader :samples
9
+
10
+ def initialize
11
+ @traits = {}
12
+ @samples = []
13
+ end
14
+
15
+ def record_observation(category:, value:)
16
+ return { status: :invalid_category, category: category } unless Constants::TRAIT_CATEGORIES.include?(category)
17
+
18
+ trait = get_or_create_trait(category)
19
+ trait.record_sample!(value.clamp(0.0, 1.0))
20
+
21
+ @samples << { category: category, value: value.clamp(0.0, 1.0), recorded_at: Time.now.utc }
22
+ @samples.shift while @samples.size > Constants::MAX_SAMPLES
23
+
24
+ {
25
+ status: :recorded,
26
+ category: category,
27
+ baseline: trait.baseline,
28
+ variance: trait.variance,
29
+ samples: trait.sample_count
30
+ }
31
+ end
32
+
33
+ def verify_identity(observations:)
34
+ return { match_score: 0.0, verdict: :insufficient_data } if observations.empty? || @traits.empty?
35
+
36
+ scored = score_observations(observations)
37
+ return { match_score: 0.0, verdict: :insufficient_data } if scored.empty?
38
+
39
+ score = (scored.sum / scored.size).round(10)
40
+ { match_score: score, verdict: score_verdict(score), observations_checked: scored.size }
41
+ end
42
+
43
+ def trait_profile
44
+ @traits.transform_values(&:baseline)
45
+ end
46
+
47
+ def strongest_traits(top_n = 3)
48
+ @traits.values
49
+ .sort_by { |t| -t.baseline }
50
+ .first(top_n)
51
+ .map(&:to_h)
52
+ end
53
+
54
+ def weakest_traits(top_n = 3)
55
+ @traits.values
56
+ .sort_by(&:baseline)
57
+ .first(top_n)
58
+ .map(&:to_h)
59
+ end
60
+
61
+ def identity_confidence
62
+ return 0.0 if @traits.empty?
63
+
64
+ stable_count = @traits.values.count(&:stable?)
65
+ sampled = @traits.values.select { |t| t.sample_count.positive? }
66
+ return 0.0 if sampled.empty?
67
+
68
+ coverage = sampled.size.to_f / Constants::TRAIT_CATEGORIES.size
69
+ stability = stable_count.to_f / @traits.size
70
+
71
+ ((coverage * 0.6) + (stability * 0.4)).round(10).clamp(0.0, 1.0)
72
+ end
73
+
74
+ def identity_label
75
+ Constants.identity_label_for(identity_confidence)
76
+ end
77
+
78
+ def anomaly_check(category:, value:)
79
+ trait = @traits[category]
80
+ return { anomaly: false, reason: :no_baseline } unless trait
81
+
82
+ dev = trait.deviation_from(value.clamp(0.0, 1.0))
83
+ anomaly = dev >= Constants::DEVIATION_THRESHOLD
84
+ {
85
+ anomaly: anomaly,
86
+ category: category,
87
+ value: value.clamp(0.0, 1.0),
88
+ baseline: trait.baseline,
89
+ deviation: dev.round(10),
90
+ threshold: Constants::DEVIATION_THRESHOLD
91
+ }
92
+ end
93
+
94
+ def fingerprint_hash
95
+ return nil if @traits.empty?
96
+
97
+ profile_string = Constants::TRAIT_CATEGORIES.map do |cat|
98
+ t = @traits[cat]
99
+ t ? "#{cat}:#{t.baseline.round(6)}" : "#{cat}:nil"
100
+ end.join('|')
101
+
102
+ require 'digest'
103
+ ::Digest::SHA256.hexdigest(profile_string)[0, 16]
104
+ end
105
+
106
+ def trait_count
107
+ @traits.size
108
+ end
109
+
110
+ def sample_count
111
+ @samples.size
112
+ end
113
+
114
+ def fingerprint_report
115
+ {
116
+ fingerprint_hash: fingerprint_hash,
117
+ identity_confidence: identity_confidence,
118
+ identity_label: identity_label,
119
+ trait_count: trait_count,
120
+ sample_count: @samples.size,
121
+ traits: @traits.transform_values(&:to_h)
122
+ }
123
+ end
124
+
125
+ def to_h
126
+ fingerprint_report
127
+ end
128
+
129
+ private
130
+
131
+ def score_observations(observations)
132
+ observations.filter_map do |obs|
133
+ cat = obs[:category]
134
+ val = obs[:value]
135
+ trait = @traits[cat]
136
+ next unless trait && Constants::TRAIT_CATEGORIES.include?(cat)
137
+
138
+ dev = trait.deviation_from(val.clamp(0.0, 1.0))
139
+ [1.0 - (dev / [Constants::DEVIATION_THRESHOLD, 0.001].max), 0.0].max.clamp(0.0, 1.0)
140
+ end
141
+ end
142
+
143
+ def score_verdict(score)
144
+ if score >= 0.7
145
+ :verified
146
+ elsif score >= 0.4
147
+ :uncertain
148
+ else
149
+ :mismatch
150
+ end
151
+ end
152
+
153
+ def get_or_create_trait(category)
154
+ @traits[category] ||= CognitiveTrait.new(category: category)
155
+ if @traits.size > Constants::MAX_TRAITS
156
+ oldest_key = @traits.min_by { |_, t| t.last_updated }.first
157
+ @traits.delete(oldest_key)
158
+ end
159
+ @traits[category]
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFingerprint
6
+ module Runners
7
+ module CognitiveFingerprint
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def record_observation(category:, value:, **)
12
+ category = category.to_sym
13
+ result = fingerprint_engine.record_observation(category: category, value: value.to_f)
14
+ Legion::Logging.debug "[cognitive_fingerprint] record category=#{category} " \
15
+ "baseline=#{result[:baseline]&.round(4)} samples=#{result[:samples]}"
16
+ result
17
+ end
18
+
19
+ def verify_identity(observations:, **)
20
+ parsed = Array(observations).map do |obs|
21
+ { category: obs[:category].to_sym, value: obs[:value].to_f }
22
+ end
23
+ result = fingerprint_engine.verify_identity(observations: parsed)
24
+ Legion::Logging.info "[cognitive_fingerprint] verify score=#{result[:match_score]&.round(4)} " \
25
+ "verdict=#{result[:verdict]}"
26
+ result
27
+ end
28
+
29
+ def anomaly_check(category:, value:, **)
30
+ result = fingerprint_engine.anomaly_check(category: category.to_sym, value: value.to_f)
31
+ if result[:anomaly]
32
+ Legion::Logging.warn "[cognitive_fingerprint] anomaly category=#{category} " \
33
+ "deviation=#{result[:deviation]&.round(4)}"
34
+ end
35
+ result
36
+ end
37
+
38
+ def trait_profile(**)
39
+ { profile: fingerprint_engine.trait_profile }
40
+ end
41
+
42
+ def strongest_traits(top_n: 3, **)
43
+ { traits: fingerprint_engine.strongest_traits(top_n.to_i) }
44
+ end
45
+
46
+ def weakest_traits(top_n: 3, **)
47
+ { traits: fingerprint_engine.weakest_traits(top_n.to_i) }
48
+ end
49
+
50
+ def identity_confidence(**)
51
+ confidence = fingerprint_engine.identity_confidence
52
+ label = fingerprint_engine.identity_label
53
+ Legion::Logging.debug "[cognitive_fingerprint] confidence=#{confidence.round(4)} label=#{label}"
54
+ { confidence: confidence, label: label }
55
+ end
56
+
57
+ def fingerprint_hash(**)
58
+ { fingerprint_hash: fingerprint_engine.fingerprint_hash }
59
+ end
60
+
61
+ def fingerprint_report(**)
62
+ fingerprint_engine.fingerprint_report
63
+ end
64
+
65
+ def fingerprint_status(**)
66
+ {
67
+ trait_count: fingerprint_engine.trait_count,
68
+ sample_count: fingerprint_engine.sample_count,
69
+ label: fingerprint_engine.identity_label
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def fingerprint_engine
76
+ @fingerprint_engine ||= Helpers::FingerprintEngine.new
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFingerprint
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_fingerprint/version'
4
+ require 'legion/extensions/cognitive_fingerprint/helpers/constants'
5
+ require 'legion/extensions/cognitive_fingerprint/helpers/cognitive_trait'
6
+ require 'legion/extensions/cognitive_fingerprint/helpers/fingerprint_engine'
7
+ require 'legion/extensions/cognitive_fingerprint/runners/cognitive_fingerprint'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveFingerprint
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_fingerprint/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitiveFingerprint::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to record_observation' do
9
+ expect(client).to respond_to(:record_observation)
10
+ end
11
+
12
+ it 'responds to verify_identity' do
13
+ expect(client).to respond_to(:verify_identity)
14
+ end
15
+
16
+ it 'responds to anomaly_check' do
17
+ expect(client).to respond_to(:anomaly_check)
18
+ end
19
+
20
+ it 'responds to trait_profile' do
21
+ expect(client).to respond_to(:trait_profile)
22
+ end
23
+
24
+ it 'responds to strongest_traits' do
25
+ expect(client).to respond_to(:strongest_traits)
26
+ end
27
+
28
+ it 'responds to weakest_traits' do
29
+ expect(client).to respond_to(:weakest_traits)
30
+ end
31
+
32
+ it 'responds to identity_confidence' do
33
+ expect(client).to respond_to(:identity_confidence)
34
+ end
35
+
36
+ it 'responds to fingerprint_hash' do
37
+ expect(client).to respond_to(:fingerprint_hash)
38
+ end
39
+
40
+ it 'responds to fingerprint_report' do
41
+ expect(client).to respond_to(:fingerprint_report)
42
+ end
43
+
44
+ it 'responds to fingerprint_status' do
45
+ expect(client).to respond_to(:fingerprint_status)
46
+ end
47
+
48
+ it 'each instance has independent state' do
49
+ c1 = described_class.new
50
+ c2 = described_class.new
51
+ c1.record_observation(category: :accuracy, value: 0.9)
52
+ expect(c2.fingerprint_status[:trait_count]).to eq(0)
53
+ end
54
+ end