lex-perspective-shifting 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: 1ec311143b14d434d25a2f54c6ca96e8cb5da4e3ceb7bed2d6a84f69b4a11e49
4
+ data.tar.gz: e6f8648010602650dcf9056f3cc5d825469d65394c6e1d5e84b042999562c235
5
+ SHA512:
6
+ metadata.gz: d3958db3162c613a323d06ae97fb5c67a72f22165663c6d95c8f5d9399527f87419cf50859a2d9d0489b7a4a99031aae0f8c1dc7147e03238c00093fa88489e7
7
+ data.tar.gz: 45e7d3d9e28241bc6db4cccd39047851af420a15a81fbc23c5732237c7fc4a94a18a83c4d8575b8a75458ed25f63115f762130c2e586e1f7668c1267279edd13
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/perspective_shifting/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-perspective-shifting'
7
+ spec.version = Legion::Extensions::PerspectiveShifting::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Perspective Shifting'
12
+ spec.description = 'Systematic multi-viewpoint analysis engine for brain-modeled agentic AI — ' \
13
+ 'cycles through stakeholder, ethical, temporal, and other interpretive frames ' \
14
+ 'to generate richer situational understanding'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-perspective-shifting'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ Dir.glob('{lib,spec}/**/*') + %w[lex-perspective-shifting.gemspec Gemfile]
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/perspective_shifting/helpers/constants'
4
+ require 'legion/extensions/perspective_shifting/helpers/perspective'
5
+ require 'legion/extensions/perspective_shifting/helpers/perspective_view'
6
+ require 'legion/extensions/perspective_shifting/helpers/shifting_engine'
7
+ require 'legion/extensions/perspective_shifting/runners/perspective_shifting'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module PerspectiveShifting
12
+ class Client
13
+ include Runners::PerspectiveShifting
14
+
15
+ def initialize(**)
16
+ @shifting_engine = Helpers::ShiftingEngine.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :shifting_engine
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PerspectiveShifting
6
+ module Helpers
7
+ module Constants
8
+ MAX_PERSPECTIVES = 50
9
+ MAX_SITUATIONS = 200
10
+ MAX_VIEWS_PER_SITUATION = 20
11
+ DEFAULT_EMPATHY = 0.5
12
+ MIN_PERSPECTIVES_FOR_SYNTHESIS = 2
13
+
14
+ PERSPECTIVE_TYPES = %i[
15
+ stakeholder emotional temporal cultural ethical pragmatic creative adversarial
16
+ ].freeze
17
+
18
+ PRIORITY_TYPES = %i[
19
+ safety efficiency fairness innovation stability growth autonomy compliance
20
+ ].freeze
21
+
22
+ EMPATHY_LABELS = {
23
+ (0.8..) => :deeply_empathic,
24
+ (0.6...0.8) => :empathic,
25
+ (0.4...0.6) => :moderate,
26
+ (0.2...0.4) => :limited,
27
+ (..0.2) => :detached
28
+ }.freeze
29
+
30
+ COVERAGE_LABELS = {
31
+ (0.8..) => :comprehensive,
32
+ (0.6...0.8) => :thorough,
33
+ (0.4...0.6) => :partial,
34
+ (0.2...0.4) => :narrow,
35
+ (..0.2) => :blind
36
+ }.freeze
37
+
38
+ AGREEMENT_LABELS = {
39
+ (0.8..) => :consensus,
40
+ (0.6...0.8) => :agreement,
41
+ (0.4...0.6) => :mixed,
42
+ (0.2...0.4) => :disagreement,
43
+ (..0.2) => :conflict
44
+ }.freeze
45
+
46
+ module_function
47
+
48
+ def empathy_label(value)
49
+ EMPATHY_LABELS.find { |range, _| range.include?(value) }&.last || :detached
50
+ end
51
+
52
+ def coverage_label(value)
53
+ COVERAGE_LABELS.find { |range, _| range.include?(value) }&.last || :blind
54
+ end
55
+
56
+ def agreement_label(value)
57
+ AGREEMENT_LABELS.find { |range, _| range.include?(value) }&.last || :conflict
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PerspectiveShifting
8
+ module Helpers
9
+ class Perspective
10
+ attr_reader :id, :name, :perspective_type, :priorities,
11
+ :expertise_domains, :empathy_level, :bias_toward, :created_at
12
+
13
+ def initialize(name:, perspective_type:, priorities: [], expertise_domains: [],
14
+ empathy_level: Constants::DEFAULT_EMPATHY, bias_toward: nil)
15
+ @id = SecureRandom.uuid
16
+ @name = name
17
+ @perspective_type = perspective_type
18
+ @priorities = Array(priorities)
19
+ @expertise_domains = Array(expertise_domains)
20
+ @empathy_level = empathy_level.clamp(0.0, 1.0)
21
+ @bias_toward = bias_toward
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ id: @id,
28
+ name: @name,
29
+ perspective_type: @perspective_type,
30
+ priorities: @priorities,
31
+ expertise_domains: @expertise_domains,
32
+ empathy_level: @empathy_level.round(10),
33
+ bias_toward: @bias_toward,
34
+ created_at: @created_at
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PerspectiveShifting
8
+ module Helpers
9
+ class PerspectiveView
10
+ attr_reader :id, :situation_id, :perspective_id, :valence,
11
+ :concerns, :opportunities, :confidence, :created_at
12
+
13
+ def initialize(situation_id:, perspective_id:, valence: 0.0,
14
+ concerns: [], opportunities: [], confidence: 0.5)
15
+ @id = SecureRandom.uuid
16
+ @situation_id = situation_id
17
+ @perspective_id = perspective_id
18
+ @valence = valence.clamp(-1.0, 1.0).round(10)
19
+ @concerns = Array(concerns)
20
+ @opportunities = Array(opportunities)
21
+ @confidence = confidence.clamp(0.0, 1.0).round(10)
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def positive?
26
+ @valence > 0.1
27
+ end
28
+
29
+ def negative?
30
+ @valence < -0.1
31
+ end
32
+
33
+ def neutral?
34
+ !positive? && !negative?
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ id: @id,
40
+ situation_id: @situation_id,
41
+ perspective_id: @perspective_id,
42
+ valence: @valence,
43
+ concerns: @concerns,
44
+ opportunities: @opportunities,
45
+ confidence: @confidence,
46
+ created_at: @created_at
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PerspectiveShifting
8
+ module Helpers
9
+ class ShiftingEngine
10
+ attr_reader :perspectives, :situations
11
+
12
+ def initialize
13
+ @perspectives = {}
14
+ @situations = {}
15
+ end
16
+
17
+ def add_perspective(name:, type: :stakeholder, priorities: [], empathy: Constants::DEFAULT_EMPATHY,
18
+ bias: nil, expertise_domains: [])
19
+ return { error: :too_many_perspectives } if @perspectives.size >= Constants::MAX_PERSPECTIVES
20
+ return { error: :invalid_type } unless Constants::PERSPECTIVE_TYPES.include?(type)
21
+
22
+ p = Perspective.new(
23
+ name: name,
24
+ perspective_type: type,
25
+ priorities: priorities,
26
+ expertise_domains: expertise_domains,
27
+ empathy_level: empathy,
28
+ bias_toward: bias
29
+ )
30
+ @perspectives[p.id] = p
31
+ p
32
+ end
33
+
34
+ def add_situation(content:)
35
+ return { error: :too_many_situations } if @situations.size >= Constants::MAX_SITUATIONS
36
+
37
+ id = SecureRandom.uuid
38
+ @situations[id] = { id: id, content: content, views: [], created_at: Time.now.utc }
39
+ @situations[id]
40
+ end
41
+
42
+ def generate_view(situation_id:, perspective_id:, valence: 0.0,
43
+ concerns: [], opportunities: [], confidence: 0.5)
44
+ situation = @situations[situation_id]
45
+ perspective = @perspectives[perspective_id]
46
+
47
+ return { error: :situation_not_found } unless situation
48
+ return { error: :perspective_not_found } unless perspective
49
+ return { error: :too_many_views } if situation[:views].size >= Constants::MAX_VIEWS_PER_SITUATION
50
+
51
+ view = PerspectiveView.new(
52
+ situation_id: situation_id,
53
+ perspective_id: perspective_id,
54
+ valence: valence,
55
+ concerns: concerns,
56
+ opportunities: opportunities,
57
+ confidence: confidence
58
+ )
59
+ situation[:views] << view
60
+ view
61
+ end
62
+
63
+ def views_for_situation(situation_id:)
64
+ situation = @situations[situation_id]
65
+ return [] unless situation
66
+
67
+ situation[:views]
68
+ end
69
+
70
+ def perspective_agreement(situation_id:)
71
+ views = views_for_situation(situation_id: situation_id)
72
+ return 0.0 if views.size < Constants::MIN_PERSPECTIVES_FOR_SYNTHESIS
73
+
74
+ valences = views.map(&:valence)
75
+ mean = valences.sum / valences.size.to_f
76
+ variance = valences.sum { |v| (v - mean)**2 } / valences.size.to_f
77
+ std_dev = Math.sqrt(variance)
78
+
79
+ # Agreement is inverse of normalized std_dev (max std_dev is 1.0 for symmetric ±1 distribution)
80
+ (1.0 - std_dev).clamp(0.0, 1.0).round(10)
81
+ end
82
+
83
+ def blind_spots(situation_id:)
84
+ views = views_for_situation(situation_id: situation_id)
85
+ applied_ids = views.map(&:perspective_id).uniq
86
+ applied_types = applied_ids.filter_map { |pid| @perspectives[pid]&.perspective_type }.uniq
87
+ Constants::PERSPECTIVE_TYPES - applied_types
88
+ end
89
+
90
+ def coverage_score(situation_id:)
91
+ views = views_for_situation(situation_id: situation_id)
92
+ return 0.0 if @perspectives.empty?
93
+
94
+ applied_count = views.map(&:perspective_id).uniq.size
95
+ (applied_count.to_f / @perspectives.size).clamp(0.0, 1.0).round(10)
96
+ end
97
+
98
+ def dominant_view(situation_id:)
99
+ views = views_for_situation(situation_id: situation_id)
100
+ return nil if views.empty?
101
+
102
+ views.max_by(&:confidence)
103
+ end
104
+
105
+ def synthesize(situation_id:)
106
+ views = views_for_situation(situation_id: situation_id)
107
+ return { error: :insufficient_views } if views.size < Constants::MIN_PERSPECTIVES_FOR_SYNTHESIS
108
+
109
+ total_confidence = views.sum(&:confidence)
110
+ return { error: :zero_confidence } if total_confidence.zero?
111
+
112
+ weighted_valence = views.sum { |v| v.valence * v.confidence } / total_confidence
113
+ all_concerns = views.flat_map(&:concerns).uniq
114
+ all_opportunities = views.flat_map(&:opportunities).uniq
115
+ agreement = perspective_agreement(situation_id: situation_id)
116
+
117
+ {
118
+ situation_id: situation_id,
119
+ weighted_valence: weighted_valence.round(10),
120
+ concerns: all_concerns,
121
+ opportunities: all_opportunities,
122
+ agreement: agreement.round(10),
123
+ agreement_label: Constants.agreement_label(agreement),
124
+ view_count: views.size,
125
+ coverage_score: coverage_score(situation_id: situation_id),
126
+ coverage_label: Constants.coverage_label(coverage_score(situation_id: situation_id))
127
+ }
128
+ end
129
+
130
+ def most_divergent_pair(situation_id:)
131
+ views = views_for_situation(situation_id: situation_id)
132
+ return nil if views.size < 2
133
+
134
+ max_diff = -1.0
135
+ pair = nil
136
+
137
+ views.combination(2) do |a, b|
138
+ diff = (a.valence - b.valence).abs
139
+ if diff > max_diff
140
+ max_diff = diff
141
+ pair = [a, b]
142
+ end
143
+ end
144
+
145
+ pair
146
+ end
147
+
148
+ def to_h
149
+ {
150
+ perspective_count: @perspectives.size,
151
+ situation_count: @situations.size,
152
+ perspectives: @perspectives.values.map(&:to_h),
153
+ situations: @situations.values.map do |s|
154
+ s.merge(views: s[:views].map(&:to_h))
155
+ end
156
+ }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PerspectiveShifting
8
+ module Runners
9
+ module PerspectiveShifting
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex)
12
+
13
+ # --- Perspective management ---
14
+
15
+ def add_perspective(name:, type: :stakeholder, priorities: [], empathy: Helpers::Constants::DEFAULT_EMPATHY,
16
+ bias: nil, expertise_domains: [], engine: nil, **)
17
+ eng = engine || shifting_engine
18
+ result = eng.add_perspective(name: name, type: type, priorities: priorities,
19
+ empathy: empathy, bias: bias, expertise_domains: expertise_domains)
20
+ return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
21
+
22
+ Legion::Logging.debug "[perspective_shifting] added perspective name=#{name} type=#{type} id=#{result.id[0..7]}"
23
+ { success: true, perspective: result.to_h }
24
+ end
25
+
26
+ def list_perspectives(engine: nil, **)
27
+ eng = engine || shifting_engine
28
+ persp = eng.perspectives.values.map(&:to_h)
29
+ Legion::Logging.debug "[perspective_shifting] list_perspectives count=#{persp.size}"
30
+ { success: true, perspectives: persp, count: persp.size }
31
+ end
32
+
33
+ def get_perspective(perspective_id:, engine: nil, **)
34
+ eng = engine || shifting_engine
35
+ persp = eng.perspectives[perspective_id]
36
+ if persp
37
+ { success: true, found: true, perspective: persp.to_h }
38
+ else
39
+ Legion::Logging.debug "[perspective_shifting] perspective not found id=#{perspective_id[0..7]}"
40
+ { success: false, found: false }
41
+ end
42
+ end
43
+
44
+ # --- Situation management ---
45
+
46
+ def add_situation(content:, engine: nil, **)
47
+ eng = engine || shifting_engine
48
+ result = eng.add_situation(content: content)
49
+ return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
50
+
51
+ Legion::Logging.debug "[perspective_shifting] added situation id=#{result[:id][0..7]}"
52
+ { success: true, situation_id: result[:id], content: result[:content] }
53
+ end
54
+
55
+ def list_situations(engine: nil, **)
56
+ eng = engine || shifting_engine
57
+ sits = eng.situations.values.map { |s| s.merge(views: s[:views].map(&:to_h)) }
58
+ Legion::Logging.debug "[perspective_shifting] list_situations count=#{sits.size}"
59
+ { success: true, situations: sits, count: sits.size }
60
+ end
61
+
62
+ # --- View generation and retrieval ---
63
+
64
+ def generate_view(situation_id:, perspective_id:, valence: 0.0,
65
+ concerns: [], opportunities: [], confidence: 0.5, engine: nil, **)
66
+ eng = engine || shifting_engine
67
+ result = eng.generate_view(
68
+ situation_id: situation_id,
69
+ perspective_id: perspective_id,
70
+ valence: valence,
71
+ concerns: concerns,
72
+ opportunities: opportunities,
73
+ confidence: confidence
74
+ )
75
+ return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
76
+
77
+ Legion::Logging.debug "[perspective_shifting] generated view id=#{result.id[0..7]} " \
78
+ "valence=#{result.valence.round(2)}"
79
+ { success: true, view: result.to_h }
80
+ end
81
+
82
+ def views_for_situation(situation_id:, engine: nil, **)
83
+ eng = engine || shifting_engine
84
+ views = eng.views_for_situation(situation_id: situation_id)
85
+ Legion::Logging.debug "[perspective_shifting] views_for_situation id=#{situation_id[0..7]} count=#{views.size}"
86
+ { success: true, views: views.map(&:to_h), count: views.size }
87
+ end
88
+
89
+ # --- Analysis ---
90
+
91
+ def perspective_agreement(situation_id:, engine: nil, **)
92
+ eng = engine || shifting_engine
93
+ score = eng.perspective_agreement(situation_id: situation_id)
94
+ label = Helpers::Constants.agreement_label(score)
95
+ Legion::Logging.debug "[perspective_shifting] agreement situation=#{situation_id[0..7]} score=#{score.round(2)} label=#{label}"
96
+ { success: true, situation_id: situation_id, agreement: score, label: label }
97
+ end
98
+
99
+ def blind_spots(situation_id:, engine: nil, **)
100
+ eng = engine || shifting_engine
101
+ spots = eng.blind_spots(situation_id: situation_id)
102
+ Legion::Logging.debug "[perspective_shifting] blind_spots situation=#{situation_id[0..7]} count=#{spots.size}"
103
+ { success: true, situation_id: situation_id, blind_spots: spots, count: spots.size }
104
+ end
105
+
106
+ def coverage_score(situation_id:, engine: nil, **)
107
+ eng = engine || shifting_engine
108
+ score = eng.coverage_score(situation_id: situation_id)
109
+ label = Helpers::Constants.coverage_label(score)
110
+ Legion::Logging.debug "[perspective_shifting] coverage situation=#{situation_id[0..7]} score=#{score.round(2)} label=#{label}"
111
+ { success: true, situation_id: situation_id, coverage: score, label: label }
112
+ end
113
+
114
+ def dominant_view(situation_id:, engine: nil, **)
115
+ eng = engine || shifting_engine
116
+ view = eng.dominant_view(situation_id: situation_id)
117
+ if view
118
+ Legion::Logging.debug "[perspective_shifting] dominant_view situation=#{situation_id[0..7]} confidence=#{view.confidence.round(2)}"
119
+ { success: true, found: true, view: view.to_h }
120
+ else
121
+ { success: true, found: false }
122
+ end
123
+ end
124
+
125
+ def synthesize(situation_id:, engine: nil, **)
126
+ eng = engine || shifting_engine
127
+ result = eng.synthesize(situation_id: situation_id)
128
+ return { success: false, error: result[:error] } if result[:error]
129
+
130
+ Legion::Logging.info "[perspective_shifting] synthesized situation=#{situation_id[0..7]} " \
131
+ "valence=#{result[:weighted_valence].round(2)} views=#{result[:view_count]}"
132
+ result.merge(success: true)
133
+ end
134
+
135
+ def most_divergent_pair(situation_id:, engine: nil, **)
136
+ eng = engine || shifting_engine
137
+ pair = eng.most_divergent_pair(situation_id: situation_id)
138
+ unless pair
139
+ Legion::Logging.debug '[perspective_shifting] most_divergent_pair: not enough views'
140
+ return { success: true, found: false }
141
+ end
142
+
143
+ divergence = (pair[0].valence - pair[1].valence).abs.round(10)
144
+ Legion::Logging.debug "[perspective_shifting] most_divergent_pair divergence=#{divergence.round(2)}"
145
+ { success: true, found: true, views: pair.map(&:to_h), divergence: divergence }
146
+ end
147
+
148
+ def engine_status(engine: nil, **)
149
+ eng = engine || shifting_engine
150
+ Legion::Logging.debug '[perspective_shifting] engine_status'
151
+ { success: true }.merge(eng.to_h)
152
+ end
153
+
154
+ private
155
+
156
+ def shifting_engine
157
+ @shifting_engine ||= Helpers::ShiftingEngine.new
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PerspectiveShifting
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/perspective_shifting/version'
4
+ require 'legion/extensions/perspective_shifting/helpers/constants'
5
+ require 'legion/extensions/perspective_shifting/helpers/perspective'
6
+ require 'legion/extensions/perspective_shifting/helpers/perspective_view'
7
+ require 'legion/extensions/perspective_shifting/helpers/shifting_engine'
8
+ require 'legion/extensions/perspective_shifting/runners/perspective_shifting'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module PerspectiveShifting
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/perspective_shifting/client'
4
+
5
+ RSpec.describe Legion::Extensions::PerspectiveShifting::Client do
6
+ it 'responds to all runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:add_perspective)
9
+ expect(client).to respond_to(:list_perspectives)
10
+ expect(client).to respond_to(:get_perspective)
11
+ expect(client).to respond_to(:add_situation)
12
+ expect(client).to respond_to(:list_situations)
13
+ expect(client).to respond_to(:generate_view)
14
+ expect(client).to respond_to(:views_for_situation)
15
+ expect(client).to respond_to(:perspective_agreement)
16
+ expect(client).to respond_to(:blind_spots)
17
+ expect(client).to respond_to(:coverage_score)
18
+ expect(client).to respond_to(:dominant_view)
19
+ expect(client).to respond_to(:synthesize)
20
+ expect(client).to respond_to(:most_divergent_pair)
21
+ expect(client).to respond_to(:engine_status)
22
+ end
23
+
24
+ it 'initializes with its own isolated engine per instance' do
25
+ c1 = described_class.new
26
+ c2 = described_class.new
27
+ c1.add_perspective(name: 'CEO', type: :stakeholder)
28
+ expect(c2.list_perspectives[:count]).to eq(0)
29
+ end
30
+ end