lex-phenomenal-binding 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: ab9e772cecd763834bbf765c3fae0d38c0a159d013e7fc5e7bcd998c45e679aa
4
+ data.tar.gz: b68e2b016e3b06def096fa89dc5c1dc2443ad43c4a2758888a63c1fea7e59107
5
+ SHA512:
6
+ metadata.gz: 9100dd38955e0dd3c4e0f94ebacff4cbcf9eceb64b9420b56e941ba5237500cf07043ea7839160a62f8d410241c37bb37fe323f9b3c9ef8eb43f954b75b25e6e
7
+ data.tar.gz: 1a434db26488ebe3ea1dea2928ef5e7716c85a5bca4e82a8cb9e3b005a200632704eb76fc880ff99dec47019847139fbc12a32d698eae9b05fa0d585b6284aac
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) 2024 Esity
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,82 @@
1
+ # lex-phenomenal-binding
2
+
3
+ Phenomenal binding model for the LegionIO cognitive architecture. Integrates disparate cognitive streams into unified moments of experience.
4
+
5
+ ## What It Does
6
+
7
+ Models the binding problem from cognitive science: how separate streams of processing (perception, thought, emotion, memory, intention, prediction) are grouped into coherent, unified experiences. Maintains a registry of named streams and binding units. Each binding tracks coherence (decays over time, reinforced by attention) and provides a fragmentation index indicating how much of the cognitive state is unintegrated.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ client = Legion::Extensions::PhenomenalBinding::Client.new
13
+
14
+ # Register cognitive streams
15
+ r1 = client.register_stream(stream_type: :emotion, content: 'mild_anticipation', salience: 0.7)
16
+ r2 = client.register_stream(stream_type: :prediction, content: 'success_likely', salience: 0.8)
17
+ r3 = client.register_stream(stream_type: :memory, content: 'prior_success_trace', salience: 0.6)
18
+
19
+ s1_id = r1[:stream][:id]
20
+ s2_id = r2[:stream][:id]
21
+ s3_id = r3[:stream][:id]
22
+
23
+ # Create a binding grouping the streams
24
+ b = client.create_binding(
25
+ stream_ids: [s1_id, s2_id, s3_id],
26
+ binding_type: :emotional,
27
+ attention_weight: 0.8
28
+ )
29
+ binding_id = b[:binding][:id]
30
+
31
+ # Reinforce the binding with attention
32
+ client.reinforce_binding(binding_id: binding_id)
33
+ # => { status: :reinforced, binding_id: ..., coherence: 0.78 }
34
+
35
+ # Check unified experience
36
+ client.unified_experience
37
+ # => { unified_experience: { binding_type: :emotional, coherence: 0.78, stream_count: 3, ... } }
38
+
39
+ # Check fragmentation
40
+ client.fragmentation_index
41
+ # => { fragmentation_index: 0.0 }
42
+
43
+ # Full consciousness report
44
+ client.consciousness_report
45
+ # => { unified_experience: {...}, fragmentation_index: 0.0,
46
+ # coherence_distribution: { unified: 1, coherent: 0, ... },
47
+ # stream_count: 3, binding_count: 1, unbound_count: 0 }
48
+
49
+ # Decay and prune on each tick
50
+ client.decay_all
51
+ client.prune_incoherent
52
+ ```
53
+
54
+ ## Binding Types
55
+
56
+ `:perceptual`, `:conceptual`, `:temporal`, `:narrative`, `:emotional`
57
+
58
+ ## Stream Types
59
+
60
+ `:perception`, `:thought`, `:emotion`, `:memory`, `:intention`, `:prediction`
61
+
62
+ ## Coherence Labels
63
+
64
+ | Label | Coherence |
65
+ |-------|-----------|
66
+ | `:unified` | >= 0.8 |
67
+ | `:coherent` | 0.6 – 0.8 |
68
+ | `:fragmented` | 0.4 – 0.6 |
69
+ | `:dissociated` | 0.2 – 0.4 |
70
+ | `:unbound` | < 0.2 |
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ bundle install
76
+ bundle exec rspec
77
+ bundle exec rubocop
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/phenomenal_binding/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-phenomenal-binding'
7
+ spec.version = Legion::Extensions::PhenomenalBinding::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Phenomenal Binding'
12
+ spec.description = 'Phenomenal binding engine for LegionIO — unifies disparate cognitive streams into coherent conscious experience'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-phenomenal-binding'
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-phenomenal-binding'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-phenomenal-binding'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-phenomenal-binding'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-phenomenal-binding/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-phenomenal-binding.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/phenomenal_binding/runners/phenomenal_binding'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PhenomenalBinding
8
+ class Client
9
+ include Runners::PhenomenalBinding
10
+
11
+ def initialize(**)
12
+ @engine = Helpers::BindingEngine.new
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :engine
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PhenomenalBinding
6
+ module Helpers
7
+ class BindingEngine
8
+ include Constants
9
+
10
+ def initialize
11
+ @streams = {}
12
+ @bindings = {}
13
+ end
14
+
15
+ def register_stream(stream_type:, content:, salience: DEFAULT_SALIENCE, domain: nil)
16
+ prune_streams_if_full
17
+ stream = Stream.new(
18
+ stream_type: stream_type,
19
+ content: content,
20
+ salience: salience,
21
+ domain: domain
22
+ )
23
+ @streams[stream.id] = stream
24
+ stream
25
+ end
26
+
27
+ def create_binding(stream_ids:, binding_type:, attention_weight: 0.5)
28
+ prune_bindings_if_full
29
+ valid_ids = stream_ids.select { |id| @streams.key?(id) }
30
+ coherence = compute_initial_coherence(valid_ids)
31
+ binding = BindingUnit.new(
32
+ stream_ids: valid_ids,
33
+ binding_type: binding_type,
34
+ coherence: coherence,
35
+ attention_weight: attention_weight
36
+ )
37
+ @bindings[binding.id] = binding
38
+ binding
39
+ end
40
+
41
+ def reinforce_binding(binding_id:)
42
+ binding = @bindings[binding_id]
43
+ return { status: :not_found, binding_id: binding_id } unless binding
44
+
45
+ binding.reinforce!
46
+ { status: :reinforced, binding_id: binding_id, coherence: binding.coherence }
47
+ end
48
+
49
+ def dissolve_binding(binding_id:)
50
+ return { status: :not_found, binding_id: binding_id } unless @bindings.key?(binding_id)
51
+
52
+ @bindings.delete(binding_id)
53
+ { status: :dissolved, binding_id: binding_id }
54
+ end
55
+
56
+ def unified_experience
57
+ coherent = @bindings.values.select(&:coherent?)
58
+ return nil if coherent.empty?
59
+
60
+ coherent.max_by { |b| b.coherence * b.attention_weight }
61
+ end
62
+
63
+ def fragmentation_index
64
+ total = @streams.size
65
+ return 0.0 if total.zero?
66
+
67
+ (unbound_streams.size.to_f / total).round(10)
68
+ end
69
+
70
+ def binding_by_type(binding_type:)
71
+ @bindings.values.select { |b| b.binding_type == binding_type }
72
+ end
73
+
74
+ def streams_for_binding(binding_id:)
75
+ binding = @bindings[binding_id]
76
+ return [] unless binding
77
+
78
+ binding.stream_ids.filter_map { |id| @streams[id] }
79
+ end
80
+
81
+ def unbound_streams
82
+ bound_ids = @bindings.values.flat_map(&:stream_ids).to_set
83
+ @streams.values.reject { |s| bound_ids.include?(s.id) }
84
+ end
85
+
86
+ def decay_all
87
+ @bindings.each_value(&:decay!)
88
+ end
89
+
90
+ def prune_incoherent
91
+ @bindings.delete_if { |_, b| !b.coherent? }
92
+ end
93
+
94
+ def consciousness_report
95
+ experience = unified_experience
96
+ distribution = coherence_distribution
97
+
98
+ {
99
+ unified_experience: experience&.to_h,
100
+ fragmentation_index: fragmentation_index,
101
+ coherence_distribution: distribution,
102
+ stream_count: @streams.size,
103
+ binding_count: @bindings.size,
104
+ unbound_count: unbound_streams.size
105
+ }
106
+ end
107
+
108
+ def to_h
109
+ {
110
+ stream_count: @streams.size,
111
+ binding_count: @bindings.size,
112
+ unbound_count: unbound_streams.size,
113
+ fragmentation: fragmentation_index
114
+ }
115
+ end
116
+
117
+ private
118
+
119
+ def compute_initial_coherence(stream_ids)
120
+ return 0.0 if stream_ids.empty?
121
+
122
+ saliences = stream_ids.filter_map { |id| @streams[id]&.salience }
123
+ return 0.0 if saliences.empty?
124
+
125
+ (saliences.sum / saliences.size).round(10)
126
+ end
127
+
128
+ def coherence_distribution
129
+ Constants::COHERENCE_LABELS.to_h do |_range, label|
130
+ count = @bindings.values.count { |b| b.coherence_label == label }
131
+ [label, count]
132
+ end
133
+ end
134
+
135
+ def prune_streams_if_full
136
+ return if @streams.size < MAX_STREAMS
137
+
138
+ oldest = @streams.values.min_by(&:created_at)
139
+ @streams.delete(oldest.id) if oldest
140
+ end
141
+
142
+ def prune_bindings_if_full
143
+ return if @bindings.size < MAX_BINDINGS
144
+
145
+ oldest = @bindings.values.min_by(&:created_at)
146
+ @bindings.delete(oldest.id) if oldest
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PhenomenalBinding
8
+ module Helpers
9
+ class BindingUnit
10
+ include Constants
11
+
12
+ attr_reader :id, :stream_ids, :binding_type, :coherence, :attention_weight, :created_at
13
+
14
+ def initialize(stream_ids:, binding_type:, coherence:, attention_weight: 0.5)
15
+ @id = ::SecureRandom.uuid
16
+ @stream_ids = stream_ids.dup
17
+ @binding_type = binding_type
18
+ @coherence = coherence.clamp(0.0, 1.0)
19
+ @attention_weight = attention_weight.clamp(0.0, 1.0)
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def add_stream(stream_id:)
24
+ @stream_ids << stream_id unless @stream_ids.include?(stream_id)
25
+ end
26
+
27
+ def remove_stream(stream_id:)
28
+ @stream_ids.delete(stream_id)
29
+ end
30
+
31
+ def reinforce!
32
+ @coherence = (@coherence + BINDING_BOOST).clamp(0.0, 1.0)
33
+ end
34
+
35
+ def decay!
36
+ @coherence = (@coherence - BINDING_DECAY).clamp(0.0, 1.0)
37
+ end
38
+
39
+ def coherent?
40
+ @coherence >= COHERENCE_THRESHOLD
41
+ end
42
+
43
+ def coherence_label
44
+ COHERENCE_LABELS.find { |range, _| range.cover?(@coherence) }&.last || :unbound
45
+ end
46
+
47
+ def stream_count
48
+ @stream_ids.size
49
+ end
50
+
51
+ def to_h
52
+ {
53
+ id: @id,
54
+ stream_ids: @stream_ids.dup,
55
+ binding_type: @binding_type,
56
+ coherence: @coherence.round(10),
57
+ attention_weight: @attention_weight.round(10),
58
+ coherent: coherent?,
59
+ coherence_label: coherence_label,
60
+ stream_count: stream_count,
61
+ created_at: @created_at
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PhenomenalBinding
6
+ module Helpers
7
+ module Constants
8
+ MAX_STREAMS = 100
9
+ MAX_BINDINGS = 200
10
+
11
+ COHERENCE_THRESHOLD = 0.6
12
+ BINDING_DECAY = 0.03
13
+ BINDING_BOOST = 0.08
14
+ DEFAULT_SALIENCE = 0.5
15
+
16
+ BINDING_TYPES = %i[perceptual conceptual temporal narrative emotional].freeze
17
+
18
+ COHERENCE_LABELS = {
19
+ (0.8..) => :unified,
20
+ (0.6...0.8) => :coherent,
21
+ (0.4...0.6) => :fragmented,
22
+ (0.2...0.4) => :dissociated,
23
+ (..0.2) => :unbound
24
+ }.freeze
25
+
26
+ STREAM_TYPES = %i[perception thought emotion memory intention prediction].freeze
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PhenomenalBinding
8
+ module Helpers
9
+ class Stream
10
+ include Constants
11
+
12
+ attr_reader :id, :stream_type, :content, :salience, :timestamp, :domain, :created_at
13
+
14
+ def initialize(stream_type:, content:, salience: DEFAULT_SALIENCE, domain: nil, timestamp: nil)
15
+ @id = ::SecureRandom.uuid
16
+ @stream_type = stream_type
17
+ @content = content
18
+ @salience = salience.clamp(0.0, 1.0)
19
+ @domain = domain
20
+ @timestamp = timestamp || Time.now.utc
21
+ @created_at = Time.now.utc
22
+ end
23
+
24
+ def salient?
25
+ @salience >= DEFAULT_SALIENCE
26
+ end
27
+
28
+ def fresh?(window: 30)
29
+ (Time.now.utc - created_at) <= window
30
+ end
31
+
32
+ def to_h
33
+ {
34
+ id: @id,
35
+ stream_type: @stream_type,
36
+ content: @content,
37
+ salience: @salience.round(10),
38
+ domain: @domain,
39
+ timestamp: @timestamp,
40
+ created_at: @created_at
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PhenomenalBinding
6
+ module Runners
7
+ module PhenomenalBinding
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def register_stream(stream_type:, content:, salience: Helpers::Constants::DEFAULT_SALIENCE,
12
+ domain: nil, **)
13
+ stream = engine.register_stream(
14
+ stream_type: stream_type,
15
+ content: content,
16
+ salience: salience,
17
+ domain: domain
18
+ )
19
+ Legion::Logging.debug "[phenomenal_binding] register_stream: type=#{stream_type} " \
20
+ "salience=#{stream.salience.round(2)} domain=#{domain}"
21
+ { status: :registered, stream: stream.to_h }
22
+ end
23
+
24
+ def create_binding(stream_ids:, binding_type:, attention_weight: 0.5, **)
25
+ result = engine.create_binding(
26
+ stream_ids: stream_ids,
27
+ binding_type: binding_type,
28
+ attention_weight: attention_weight
29
+ )
30
+ Legion::Logging.debug "[phenomenal_binding] create_binding: type=#{binding_type} " \
31
+ "streams=#{result.stream_count} coherence=#{result.coherence.round(2)}"
32
+ { status: :bound, binding: result.to_h }
33
+ end
34
+
35
+ def reinforce_binding(binding_id:, **)
36
+ result = engine.reinforce_binding(binding_id: binding_id)
37
+ Legion::Logging.debug "[phenomenal_binding] reinforce_binding: id=#{binding_id} " \
38
+ "coherence=#{result[:coherence]&.round(2)}"
39
+ result
40
+ end
41
+
42
+ def dissolve_binding(binding_id:, **)
43
+ result = engine.dissolve_binding(binding_id: binding_id)
44
+ Legion::Logging.debug "[phenomenal_binding] dissolve_binding: id=#{binding_id} status=#{result[:status]}"
45
+ result
46
+ end
47
+
48
+ def unified_experience(**)
49
+ experience = engine.unified_experience
50
+ Legion::Logging.debug '[phenomenal_binding] unified_experience: ' \
51
+ "coherence=#{experience&.coherence&.round(2) || 'none'}"
52
+ { unified_experience: experience&.to_h }
53
+ end
54
+
55
+ def fragmentation_index(**)
56
+ index = engine.fragmentation_index
57
+ Legion::Logging.debug "[phenomenal_binding] fragmentation_index=#{index.round(3)}"
58
+ { fragmentation_index: index }
59
+ end
60
+
61
+ def binding_by_type(binding_type:, **)
62
+ bindings = engine.binding_by_type(binding_type: binding_type)
63
+ Legion::Logging.debug "[phenomenal_binding] binding_by_type: type=#{binding_type} count=#{bindings.size}"
64
+ { binding_type: binding_type, bindings: bindings.map(&:to_h) }
65
+ end
66
+
67
+ def streams_for_binding(binding_id:, **)
68
+ streams = engine.streams_for_binding(binding_id: binding_id)
69
+ Legion::Logging.debug "[phenomenal_binding] streams_for_binding: id=#{binding_id} count=#{streams.size}"
70
+ { binding_id: binding_id, streams: streams.map(&:to_h) }
71
+ end
72
+
73
+ def unbound_streams(**)
74
+ streams = engine.unbound_streams
75
+ Legion::Logging.debug "[phenomenal_binding] unbound_streams: count=#{streams.size}"
76
+ { unbound_streams: streams.map(&:to_h) }
77
+ end
78
+
79
+ def decay_all(**)
80
+ engine.decay_all
81
+ Legion::Logging.debug '[phenomenal_binding] decay_all: all bindings decayed'
82
+ { status: :decayed }
83
+ end
84
+
85
+ def prune_incoherent(**)
86
+ engine.prune_incoherent
87
+ Legion::Logging.debug '[phenomenal_binding] prune_incoherent: incoherent bindings removed'
88
+ { status: :pruned }
89
+ end
90
+
91
+ def consciousness_report(**)
92
+ report = engine.consciousness_report
93
+ Legion::Logging.debug '[phenomenal_binding] consciousness_report: ' \
94
+ "fragmentation=#{report[:fragmentation_index].round(3)} " \
95
+ "bindings=#{report[:binding_count]}"
96
+ report
97
+ end
98
+
99
+ def engine_stats(**)
100
+ engine.to_h
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PhenomenalBinding
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/phenomenal_binding/version'
4
+ require 'legion/extensions/phenomenal_binding/helpers/constants'
5
+ require 'legion/extensions/phenomenal_binding/helpers/stream'
6
+ require 'legion/extensions/phenomenal_binding/helpers/binding_unit'
7
+ require 'legion/extensions/phenomenal_binding/helpers/binding_engine'
8
+ require 'legion/extensions/phenomenal_binding/runners/phenomenal_binding'
9
+ require 'legion/extensions/phenomenal_binding/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module PhenomenalBinding
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/phenomenal_binding/client'
4
+
5
+ RSpec.describe Legion::Extensions::PhenomenalBinding::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to runner methods' do
9
+ expect(client).to respond_to(:register_stream)
10
+ expect(client).to respond_to(:create_binding)
11
+ expect(client).to respond_to(:reinforce_binding)
12
+ expect(client).to respond_to(:dissolve_binding)
13
+ expect(client).to respond_to(:unified_experience)
14
+ expect(client).to respond_to(:fragmentation_index)
15
+ expect(client).to respond_to(:binding_by_type)
16
+ expect(client).to respond_to(:streams_for_binding)
17
+ expect(client).to respond_to(:unbound_streams)
18
+ expect(client).to respond_to(:decay_all)
19
+ expect(client).to respond_to(:prune_incoherent)
20
+ expect(client).to respond_to(:consciousness_report)
21
+ expect(client).to respond_to(:engine_stats)
22
+ end
23
+
24
+ it 'maintains independent engine state per instance' do
25
+ c1 = described_class.new
26
+ c2 = described_class.new
27
+ c1.register_stream(stream_type: :perception, content: 'only in c1')
28
+ expect(c1.engine_stats[:stream_count]).to eq(1)
29
+ expect(c2.engine_stats[:stream_count]).to eq(0)
30
+ end
31
+
32
+ it 'runs a full bind/reinforce/dissolve cycle' do
33
+ s = client.register_stream(stream_type: :thought, content: 'philosophy', salience: 0.75)
34
+ b = client.create_binding(stream_ids: [s[:stream][:id]], binding_type: :conceptual)
35
+ client.reinforce_binding(binding_id: b[:binding][:id])
36
+ expect(client.unified_experience[:unified_experience]).to be_a(Hash)
37
+ client.dissolve_binding(binding_id: b[:binding][:id])
38
+ expect(client.unified_experience[:unified_experience]).to be_nil
39
+ end
40
+
41
+ it 'builds a consciousness report across multiple streams and bindings' do
42
+ s1 = client.register_stream(stream_type: :perception, content: 'light', salience: 0.8)
43
+ s2 = client.register_stream(stream_type: :emotion, content: 'warmth', salience: 0.7)
44
+ s3 = client.register_stream(stream_type: :memory, content: 'familiar', salience: 0.9)
45
+ client.create_binding(stream_ids: [s1[:stream][:id], s2[:stream][:id]], binding_type: :emotional)
46
+ report = client.consciousness_report
47
+ expect(report[:stream_count]).to eq(3)
48
+ expect(report[:binding_count]).to eq(1)
49
+ expect(report[:unbound_count]).to eq(1) # s3 is unbound
50
+ _ = s3
51
+ end
52
+ end