lex-gestalt 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: 45bfd8dcac2d9eed154eda678859a0058dda75582c80bb3eef9f28a02b97629e
4
+ data.tar.gz: 2bd8c8a7bc03b74a923b9967ff5dff9d71ca3cbbde49d68a1c144d6e645e5f0c
5
+ SHA512:
6
+ metadata.gz: a7f9e56e02ad01c2cbba6ce1430497f863805b4b81ce001a4007d499939909c137c93546be4d4f5409f123bd4f476c895e558b4e143a8d21244bb6498259228b
7
+ data.tar.gz: 444b301abc9873ee484bdce266b067142e191dbbe8e2a7be5b35609e2bbbb3b828c8b579d27072e9d09e1ea2f41a67fc8c43a6071fd18499a26e7ff1faecc2b0
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,58 @@
1
+ # lex-gestalt
2
+
3
+ Gestalt perception modeling for the LegionIO brain-modeled cognitive architecture.
4
+
5
+ ## What It Does
6
+
7
+ Implements the Gestalt principles of perceptual organization — the tendency to perceive elements as unified wholes rather than isolated parts. Groups input elements by proximity, similarity, continuity, closure, symmetry, and common fate. Computes coherence scores for groups and detects emergent patterns where the whole is more than the sum of its parts. Supports holistic pattern recognition across structured inputs.
8
+
9
+ Based on Wertheimer, Köhler, and Koffka's Gestalt psychology.
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ client = Legion::Extensions::Gestalt::Client.new
15
+
16
+ # Register perceptual elements
17
+ client.register_element(content: 'auth_service', properties: { type: :service, domain: :security })
18
+ client.register_element(content: 'session_store', properties: { type: :storage, domain: :security })
19
+ client.register_element(content: 'token_validator', properties: { type: :service, domain: :security })
20
+
21
+ # Group by a Gestalt principle
22
+ client.apply_grouping(
23
+ element_ids: ['...', '...', '...'],
24
+ principle: :similarity
25
+ )
26
+ # => { success: true, group_id: "...", principle: :similarity, coherence: 0.8, emergence_detected: true }
27
+
28
+ # Detect emergent patterns in the group
29
+ client.detect_emergence(group_id: '...')
30
+ # => { emergence_score: 0.85, emergence_label: :strong_emergence,
31
+ # emergent_pattern: 'authentication_cluster' }
32
+
33
+ # Find elements similar to a reference
34
+ client.find_similar_elements(reference_id: '...', threshold: 0.6)
35
+
36
+ # Automatically group all ungrouped elements
37
+ client.auto_group(domain: :security)
38
+ # => { groups_formed: 2, elements_grouped: 5, ungrouped_count: 1 }
39
+
40
+ # View active groups by coherence
41
+ client.active_groups
42
+ # => { groups: [...sorted by coherence], count: 2 }
43
+
44
+ # Periodic maintenance
45
+ client.update_gestalt
46
+ ```
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ bundle install
52
+ bundle exec rspec
53
+ bundle exec rubocop
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Gestalt
6
+ class Client
7
+ include Runners::Gestalt
8
+
9
+ attr_reader :store
10
+
11
+ def initialize(store: nil, **)
12
+ @store = store || Helpers::PatternStore.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Gestalt
6
+ module Helpers
7
+ module Constants
8
+ # Minimum pattern match ratio to attempt completion
9
+ COMPLETION_THRESHOLD = 0.3
10
+
11
+ # Confidence boost for each additional matching element
12
+ MATCH_CONFIDENCE_BOOST = 0.15
13
+
14
+ # Maximum stored patterns
15
+ MAX_PATTERNS = 200
16
+
17
+ # Maximum elements per pattern
18
+ MAX_PATTERN_SIZE = 50
19
+
20
+ # Decay rate for pattern recency (older patterns contribute less)
21
+ PATTERN_DECAY = 0.01
22
+
23
+ # Minimum confidence to report a completion
24
+ MIN_COMPLETION_CONFIDENCE = 0.2
25
+
26
+ # Maximum completion candidates to return
27
+ MAX_COMPLETIONS = 5
28
+
29
+ # Grouping principles (Wertheimer's laws)
30
+ GROUPING_PRINCIPLES = %i[proximity similarity closure continuity common_fate].freeze
31
+
32
+ # Proximity threshold for grouping (normalized distance)
33
+ PROXIMITY_THRESHOLD = 0.3
34
+
35
+ # Similarity threshold for grouping
36
+ SIMILARITY_THRESHOLD = 0.6
37
+
38
+ # Reinforcement when a completion is confirmed correct
39
+ CORRECT_REINFORCEMENT = 0.1
40
+
41
+ # Penalty when a completion is wrong
42
+ INCORRECT_PENALTY = 0.15
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Gestalt
8
+ module Helpers
9
+ class Pattern
10
+ attr_reader :id, :name, :domain, :elements, :created_at
11
+ attr_accessor :strength, :match_count
12
+
13
+ def initialize(name:, elements:, domain: :general)
14
+ @id = SecureRandom.uuid
15
+ @name = name
16
+ @domain = domain
17
+ @elements = elements.first(Constants::MAX_PATTERN_SIZE)
18
+ @strength = 1.0
19
+ @match_count = 0
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def match_ratio(fragment)
24
+ return 0.0 if @elements.empty? || fragment.empty?
25
+
26
+ matches = fragment.count { |e| @elements.include?(e) }
27
+ matches.to_f / @elements.size
28
+ end
29
+
30
+ def missing_elements(fragment)
31
+ @elements - fragment
32
+ end
33
+
34
+ def complete(fragment)
35
+ ratio = match_ratio(fragment)
36
+ return nil if ratio < Constants::COMPLETION_THRESHOLD
37
+
38
+ confidence = compute_confidence(ratio)
39
+ missing = missing_elements(fragment)
40
+
41
+ @match_count += 1
42
+ {
43
+ pattern_id: @id,
44
+ pattern_name: @name,
45
+ completed: @elements,
46
+ missing: missing,
47
+ confidence: confidence.round(4),
48
+ match_ratio: ratio.round(4)
49
+ }
50
+ end
51
+
52
+ def reinforce
53
+ @strength = [@strength + Constants::CORRECT_REINFORCEMENT, 2.0].min
54
+ end
55
+
56
+ def penalize
57
+ @strength = [@strength - Constants::INCORRECT_PENALTY, 0.1].max
58
+ end
59
+
60
+ def decay
61
+ @strength = [@strength - Constants::PATTERN_DECAY, 0.1].max
62
+ end
63
+
64
+ def to_h
65
+ {
66
+ id: @id,
67
+ name: @name,
68
+ domain: @domain,
69
+ elements: @elements,
70
+ strength: @strength.round(4),
71
+ match_count: @match_count,
72
+ size: @elements.size
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def compute_confidence(ratio)
79
+ base = ratio * Constants::MATCH_CONFIDENCE_BOOST / Constants::COMPLETION_THRESHOLD
80
+ (base * @strength).clamp(0.0, 1.0)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Gestalt
6
+ module Helpers
7
+ class PatternStore
8
+ attr_reader :patterns
9
+
10
+ def initialize
11
+ @patterns = []
12
+ end
13
+
14
+ def add(name:, elements:, domain: :general)
15
+ pattern = Pattern.new(name: name, elements: elements, domain: domain)
16
+ @patterns << pattern
17
+ trim
18
+ pattern
19
+ end
20
+
21
+ def find(id)
22
+ @patterns.find { |p| p.id == id }
23
+ end
24
+
25
+ def find_by_name(name)
26
+ @patterns.select { |p| p.name == name }
27
+ end
28
+
29
+ def in_domain(domain)
30
+ @patterns.select { |p| p.domain == domain }
31
+ end
32
+
33
+ def complete(fragment, domain: nil)
34
+ candidates = domain ? in_domain(domain) : @patterns
35
+ completions = candidates.filter_map { |p| p.complete(fragment) }
36
+ completions.sort_by { |c| -c[:confidence] }.first(Constants::MAX_COMPLETIONS)
37
+ end
38
+
39
+ def group_by_proximity(items, threshold: Constants::PROXIMITY_THRESHOLD)
40
+ return [items] if items.size <= 1
41
+
42
+ groups = []
43
+ remaining = items.dup
44
+
45
+ until remaining.empty?
46
+ seed = remaining.shift
47
+ group = [seed]
48
+ remaining.reject! do |item|
49
+ if proximity(seed, item) <= threshold
50
+ group << item
51
+ true
52
+ else
53
+ false
54
+ end
55
+ end
56
+ groups << group
57
+ end
58
+ groups
59
+ end
60
+
61
+ def group_by_similarity(items, key: :type, **)
62
+ items.group_by { |item| item.is_a?(Hash) ? item[key] : item }.values
63
+ end
64
+
65
+ def reinforce(id)
66
+ pattern = find(id)
67
+ return nil unless pattern
68
+
69
+ pattern.reinforce
70
+ pattern
71
+ end
72
+
73
+ def penalize(id)
74
+ pattern = find(id)
75
+ return nil unless pattern
76
+
77
+ pattern.penalize
78
+ pattern
79
+ end
80
+
81
+ def decay_all
82
+ @patterns.each(&:decay)
83
+ end
84
+
85
+ def remove(id)
86
+ @patterns.reject! { |p| p.id == id }
87
+ end
88
+
89
+ def size
90
+ @patterns.size
91
+ end
92
+
93
+ def to_h
94
+ {
95
+ pattern_count: @patterns.size,
96
+ by_domain: @patterns.group_by(&:domain).transform_values(&:size),
97
+ total_matches: @patterns.sum(&:match_count)
98
+ }
99
+ end
100
+
101
+ private
102
+
103
+ def proximity(item_a, item_b)
104
+ return 1.0 unless item_a.is_a?(Numeric) && item_b.is_a?(Numeric)
105
+
106
+ (item_a - item_b).abs.to_f / [item_a.abs, item_b.abs, 1].max
107
+ end
108
+
109
+ def trim
110
+ return unless @patterns.size > Constants::MAX_PATTERNS
111
+
112
+ @patterns.sort_by!(&:strength)
113
+ @patterns.shift(@patterns.size - Constants::MAX_PATTERNS)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Gestalt
6
+ module Runners
7
+ module Gestalt
8
+ include Legion::Extensions::Helpers::Lex
9
+
10
+ def store
11
+ @store ||= Helpers::PatternStore.new
12
+ end
13
+
14
+ def learn_pattern(name:, elements:, domain: :general, **)
15
+ pattern = store.add(name: name, elements: elements, domain: domain)
16
+ Legion::Logging.debug "[gestalt] learned pattern=#{name} domain=#{domain} size=#{elements.size}"
17
+ { success: true, pattern: pattern.to_h }
18
+ end
19
+
20
+ def complete_pattern(fragment:, domain: nil, **)
21
+ completions = store.complete(fragment, domain: domain)
22
+ Legion::Logging.debug "[gestalt] complete: #{completions.size} candidates for #{fragment.size} elements"
23
+ {
24
+ success: true,
25
+ completions: completions,
26
+ count: completions.size,
27
+ best: completions.first
28
+ }
29
+ end
30
+
31
+ def confirm_completion(pattern_id:, correct: true, **)
32
+ result = if correct
33
+ store.reinforce(pattern_id)
34
+ else
35
+ store.penalize(pattern_id)
36
+ end
37
+ return { success: false, reason: :not_found } unless result
38
+
39
+ Legion::Logging.debug "[gestalt] #{correct ? 'reinforced' : 'penalized'} pattern=#{pattern_id}"
40
+ { success: true, pattern: result.to_h }
41
+ end
42
+
43
+ def group_items(items:, principle: :proximity, **)
44
+ groups = case principle
45
+ when :proximity
46
+ store.group_by_proximity(items)
47
+ when :similarity
48
+ store.group_by_similarity(items)
49
+ else
50
+ [items]
51
+ end
52
+ { success: true, groups: groups, group_count: groups.size, principle: principle }
53
+ end
54
+
55
+ def update_gestalt(**)
56
+ store.decay_all
57
+ Legion::Logging.debug "[gestalt] tick: patterns=#{store.size}"
58
+ { success: true, pattern_count: store.size }
59
+ end
60
+
61
+ def patterns_in_domain(domain:, **)
62
+ patterns = store.in_domain(domain)
63
+ { success: true, patterns: patterns.map(&:to_h), count: patterns.size }
64
+ end
65
+
66
+ def remove_pattern(id:, **)
67
+ store.remove(id)
68
+ { success: true }
69
+ end
70
+
71
+ def gestalt_stats(**)
72
+ { success: true, stats: store.to_h }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Gestalt
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'gestalt/version'
4
+ require_relative 'gestalt/helpers/constants'
5
+ require_relative 'gestalt/helpers/pattern'
6
+ require_relative 'gestalt/helpers/pattern_store'
7
+ require_relative 'gestalt/runners/gestalt'
8
+ require_relative 'gestalt/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Gestalt
13
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-gestalt
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 gestalt perception — completing partial patterns from context,
27
+ recognizing wholes from fragments, and grouping related signals.
28
+ email:
29
+ - matt@iverson.io
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/legion/extensions/gestalt.rb
37
+ - lib/legion/extensions/gestalt/client.rb
38
+ - lib/legion/extensions/gestalt/helpers/constants.rb
39
+ - lib/legion/extensions/gestalt/helpers/pattern.rb
40
+ - lib/legion/extensions/gestalt/helpers/pattern_store.rb
41
+ - lib/legion/extensions/gestalt/runners/gestalt.rb
42
+ - lib/legion/extensions/gestalt/version.rb
43
+ homepage: https://github.com/LegionIO/lex-gestalt
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: Gestalt pattern completion for LegionIO
65
+ test_files: []