lex-agentic-language 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.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-language.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/language/conceptual_blending/client.rb +25 -0
  8. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blend.rb +91 -0
  9. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine.rb +171 -0
  10. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/constants.rb +35 -0
  11. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space.rb +51 -0
  12. data/lib/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending.rb +106 -0
  13. data/lib/legion/extensions/agentic/language/conceptual_blending/version.rb +13 -0
  14. data/lib/legion/extensions/agentic/language/conceptual_blending.rb +20 -0
  15. data/lib/legion/extensions/agentic/language/conceptual_metaphor/client.rb +19 -0
  16. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/constants.rb +49 -0
  17. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor.rb +109 -0
  18. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine.rb +154 -0
  19. data/lib/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor.rb +107 -0
  20. data/lib/legion/extensions/agentic/language/conceptual_metaphor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/language/conceptual_metaphor.rb +19 -0
  22. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/client.rb +23 -0
  23. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/constants.rb +32 -0
  24. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame.rb +109 -0
  25. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine.rb +139 -0
  26. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance.rb +51 -0
  27. data/lib/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics.rb +108 -0
  28. data/lib/legion/extensions/agentic/language/frame_semantics/version.rb +13 -0
  29. data/lib/legion/extensions/agentic/language/frame_semantics.rb +20 -0
  30. data/lib/legion/extensions/agentic/language/grammar/client.rb +29 -0
  31. data/lib/legion/extensions/agentic/language/grammar/helpers/constants.rb +38 -0
  32. data/lib/legion/extensions/agentic/language/grammar/helpers/construal.rb +68 -0
  33. data/lib/legion/extensions/agentic/language/grammar/helpers/construction.rb +68 -0
  34. data/lib/legion/extensions/agentic/language/grammar/helpers/grammar_engine.rb +119 -0
  35. data/lib/legion/extensions/agentic/language/grammar/runners/cognitive_grammar.rb +100 -0
  36. data/lib/legion/extensions/agentic/language/grammar/version.rb +13 -0
  37. data/lib/legion/extensions/agentic/language/grammar.rb +20 -0
  38. data/lib/legion/extensions/agentic/language/inner_speech/client.rb +19 -0
  39. data/lib/legion/extensions/agentic/language/inner_speech/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/language/inner_speech/helpers/inner_voice.rb +141 -0
  41. data/lib/legion/extensions/agentic/language/inner_speech/helpers/speech_stream.rb +128 -0
  42. data/lib/legion/extensions/agentic/language/inner_speech/helpers/utterance.rb +90 -0
  43. data/lib/legion/extensions/agentic/language/inner_speech/runners/inner_speech.rb +87 -0
  44. data/lib/legion/extensions/agentic/language/inner_speech/version.rb +13 -0
  45. data/lib/legion/extensions/agentic/language/inner_speech.rb +20 -0
  46. data/lib/legion/extensions/agentic/language/language/client.rb +26 -0
  47. data/lib/legion/extensions/agentic/language/language/helpers/constants.rb +43 -0
  48. data/lib/legion/extensions/agentic/language/language/helpers/lexicon.rb +63 -0
  49. data/lib/legion/extensions/agentic/language/language/helpers/summarizer.rb +167 -0
  50. data/lib/legion/extensions/agentic/language/language/runners/language.rb +134 -0
  51. data/lib/legion/extensions/agentic/language/language/version.rb +13 -0
  52. data/lib/legion/extensions/agentic/language/language.rb +19 -0
  53. data/lib/legion/extensions/agentic/language/narrative_reasoning/client.rb +28 -0
  54. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative.rb +123 -0
  55. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine.rb +122 -0
  56. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event.rb +41 -0
  57. data/lib/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning.rb +122 -0
  58. data/lib/legion/extensions/agentic/language/narrative_reasoning/version.rb +13 -0
  59. data/lib/legion/extensions/agentic/language/narrative_reasoning.rb +18 -0
  60. data/lib/legion/extensions/agentic/language/narrator/client.rb +27 -0
  61. data/lib/legion/extensions/agentic/language/narrator/helpers/constants.rb +69 -0
  62. data/lib/legion/extensions/agentic/language/narrator/helpers/journal.rb +68 -0
  63. data/lib/legion/extensions/agentic/language/narrator/helpers/llm_enhancer.rb +105 -0
  64. data/lib/legion/extensions/agentic/language/narrator/helpers/prose.rb +122 -0
  65. data/lib/legion/extensions/agentic/language/narrator/helpers/synthesizer.rb +138 -0
  66. data/lib/legion/extensions/agentic/language/narrator/runners/narrator.rb +196 -0
  67. data/lib/legion/extensions/agentic/language/narrator/version.rb +13 -0
  68. data/lib/legion/extensions/agentic/language/narrator.rb +21 -0
  69. data/lib/legion/extensions/agentic/language/pragmatic_inference/client.rb +28 -0
  70. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/constants.rb +52 -0
  71. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine.rb +164 -0
  72. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance.rb +84 -0
  73. data/lib/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference.rb +136 -0
  74. data/lib/legion/extensions/agentic/language/pragmatic_inference/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/language/pragmatic_inference.rb +18 -0
  76. data/lib/legion/extensions/agentic/language/version.rb +11 -0
  77. data/lib/legion/extensions/agentic/language.rb +28 -0
  78. data/spec/legion/extensions/agentic/language/conceptual_blending/client_spec.rb +78 -0
  79. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blend_spec.rb +141 -0
  80. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine_spec.rb +211 -0
  81. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space_spec.rb +85 -0
  82. data/spec/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending_spec.rb +162 -0
  83. data/spec/legion/extensions/agentic/language/conceptual_metaphor/client_spec.rb +29 -0
  84. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine_spec.rb +166 -0
  85. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_spec.rb +133 -0
  86. data/spec/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor_spec.rb +133 -0
  87. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine_spec.rb +227 -0
  88. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance_spec.rb +83 -0
  89. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_spec.rb +213 -0
  90. data/spec/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics_spec.rb +155 -0
  91. data/spec/legion/extensions/agentic/language/grammar/client_spec.rb +121 -0
  92. data/spec/legion/extensions/agentic/language/grammar/cognitive_grammar_spec.rb +18 -0
  93. data/spec/legion/extensions/agentic/language/grammar/helpers/constants_spec.rb +67 -0
  94. data/spec/legion/extensions/agentic/language/grammar/helpers/construal_spec.rb +124 -0
  95. data/spec/legion/extensions/agentic/language/grammar/helpers/construction_spec.rb +155 -0
  96. data/spec/legion/extensions/agentic/language/grammar/helpers/grammar_engine_spec.rb +206 -0
  97. data/spec/legion/extensions/agentic/language/grammar/runners/cognitive_grammar_spec.rb +189 -0
  98. data/spec/legion/extensions/agentic/language/inner_speech/client_spec.rb +39 -0
  99. data/spec/legion/extensions/agentic/language/inner_speech/helpers/inner_voice_spec.rb +185 -0
  100. data/spec/legion/extensions/agentic/language/inner_speech/helpers/speech_stream_spec.rb +158 -0
  101. data/spec/legion/extensions/agentic/language/inner_speech/helpers/utterance_spec.rb +121 -0
  102. data/spec/legion/extensions/agentic/language/inner_speech/runners/inner_speech_spec.rb +102 -0
  103. data/spec/legion/extensions/agentic/language/language/client_spec.rb +20 -0
  104. data/spec/legion/extensions/agentic/language/language/helpers/constants_spec.rb +31 -0
  105. data/spec/legion/extensions/agentic/language/language/helpers/lexicon_spec.rb +116 -0
  106. data/spec/legion/extensions/agentic/language/language/helpers/summarizer_spec.rb +224 -0
  107. data/spec/legion/extensions/agentic/language/language/runners/language_spec.rb +169 -0
  108. data/spec/legion/extensions/agentic/language/narrative_reasoning/client_spec.rb +19 -0
  109. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine_spec.rb +182 -0
  110. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event_spec.rb +61 -0
  111. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_spec.rb +168 -0
  112. data/spec/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning_spec.rb +174 -0
  113. data/spec/legion/extensions/agentic/language/narrator/client_spec.rb +24 -0
  114. data/spec/legion/extensions/agentic/language/narrator/helpers/journal_spec.rb +95 -0
  115. data/spec/legion/extensions/agentic/language/narrator/helpers/llm_enhancer_spec.rb +107 -0
  116. data/spec/legion/extensions/agentic/language/narrator/helpers/prose_spec.rb +134 -0
  117. data/spec/legion/extensions/agentic/language/narrator/helpers/synthesizer_spec.rb +89 -0
  118. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_llm_spec.rb +74 -0
  119. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_spec.rb +126 -0
  120. data/spec/legion/extensions/agentic/language/pragmatic_inference/client_spec.rb +19 -0
  121. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/constants_spec.rb +73 -0
  122. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine_spec.rb +185 -0
  123. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance_spec.rb +111 -0
  124. data/spec/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference_spec.rb +231 -0
  125. data/spec/spec_helper.rb +33 -0
  126. metadata +210 -0
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEngine do
4
+ let(:engine) { described_class.new }
5
+
6
+ describe '#create_narrative' do
7
+ it 'creates a new narrative' do
8
+ narrative = engine.create_narrative(title: 'Test Story', domain: 'test')
9
+ expect(narrative).to be_a(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::Narrative)
10
+ expect(narrative.title).to eq('Test Story')
11
+ expect(narrative.domain).to eq('test')
12
+ end
13
+
14
+ it 'stores the narrative' do
15
+ narrative = engine.create_narrative(title: 'Stored')
16
+ expect(engine.get(narrative.id)).to eq(narrative)
17
+ end
18
+
19
+ it 'increments count' do
20
+ engine.create_narrative(title: 'First')
21
+ engine.create_narrative(title: 'Second')
22
+ expect(engine.count).to eq(2)
23
+ end
24
+ end
25
+
26
+ describe '#add_narrative_event' do
27
+ let(:narrative) { engine.create_narrative(title: 'Events Test') }
28
+
29
+ it 'adds an event to the narrative' do
30
+ event = engine.add_narrative_event(
31
+ narrative_id: narrative.id,
32
+ content: 'Something happened',
33
+ event_type: :action
34
+ )
35
+ expect(event).to be_a(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent)
36
+ expect(narrative.events.size).to eq(1)
37
+ end
38
+
39
+ it 'returns nil for unknown narrative' do
40
+ result = engine.add_narrative_event(
41
+ narrative_id: 'no-such-id',
42
+ content: 'x',
43
+ event_type: :action
44
+ )
45
+ expect(result).to be_nil
46
+ end
47
+
48
+ it 'returns nil for invalid event_type' do
49
+ result = engine.add_narrative_event(
50
+ narrative_id: narrative.id,
51
+ content: 'x',
52
+ event_type: :invalid_type
53
+ )
54
+ expect(result).to be_nil
55
+ end
56
+
57
+ it 'tracks total_events count' do
58
+ engine.add_narrative_event(narrative_id: narrative.id, content: 'a', event_type: :action)
59
+ engine.add_narrative_event(narrative_id: narrative.id, content: 'b', event_type: :conflict)
60
+ expect(engine.total_events).to eq(2)
61
+ end
62
+
63
+ it 'accepts all valid event types' do
64
+ described_class::EVENT_TYPES.each do |type|
65
+ result = engine.add_narrative_event(narrative_id: narrative.id, content: 'x', event_type: type)
66
+ expect(result).not_to be_nil
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#add_narrative_theme' do
72
+ let(:narrative) { engine.create_narrative(title: 'Theme Test') }
73
+
74
+ it 'adds a theme to the narrative' do
75
+ engine.add_narrative_theme(narrative_id: narrative.id, theme: 'identity')
76
+ expect(narrative.themes).to include('identity')
77
+ end
78
+
79
+ it 'returns nil for unknown narrative' do
80
+ result = engine.add_narrative_theme(narrative_id: 'unknown', theme: 'x')
81
+ expect(result).to be_nil
82
+ end
83
+ end
84
+
85
+ describe '#advance_narrative' do
86
+ let(:narrative) { engine.create_narrative(title: 'Arc Test') }
87
+
88
+ it 'advances arc stage' do
89
+ new_stage = engine.advance_narrative(narrative_id: narrative.id)
90
+ expect(new_stage).to eq(:rising_action)
91
+ end
92
+
93
+ it 'returns nil for unknown narrative' do
94
+ result = engine.advance_narrative(narrative_id: 'bad')
95
+ expect(result).to be_nil
96
+ end
97
+ end
98
+
99
+ describe '#trace_causal_chain' do
100
+ let(:narrative) { engine.create_narrative(title: 'Causal Test') }
101
+
102
+ it 'returns empty array for narrative with no causes' do
103
+ engine.add_narrative_event(narrative_id: narrative.id, content: 'start', event_type: :action)
104
+ chain = engine.trace_causal_chain(narrative_id: narrative.id)
105
+ expect(chain).to be_empty
106
+ end
107
+
108
+ it 'returns causal links when events have causes' do
109
+ evt1 = engine.add_narrative_event(narrative_id: narrative.id, content: 'start', event_type: :action)
110
+ engine.add_narrative_event(
111
+ narrative_id: narrative.id,
112
+ content: 'consequence',
113
+ event_type: :resolution,
114
+ causes: [evt1.id]
115
+ )
116
+ chain = engine.trace_causal_chain(narrative_id: narrative.id)
117
+ expect(chain.size).to eq(1)
118
+ end
119
+
120
+ it 'returns empty array for unknown narrative' do
121
+ expect(engine.trace_causal_chain(narrative_id: 'nope')).to eq([])
122
+ end
123
+ end
124
+
125
+ describe '#complete_narratives' do
126
+ it 'returns narratives at resolution stage' do
127
+ n1 = engine.create_narrative(title: 'Finished')
128
+ 5.times { n1.advance_arc! }
129
+ engine.create_narrative(title: 'In Progress')
130
+ expect(engine.complete_narratives).to include(n1)
131
+ expect(engine.complete_narratives.size).to eq(1)
132
+ end
133
+ end
134
+
135
+ describe '#by_domain' do
136
+ it 'returns narratives matching domain' do
137
+ engine.create_narrative(title: 'A', domain: 'sci-fi')
138
+ engine.create_narrative(title: 'B', domain: 'sci-fi')
139
+ engine.create_narrative(title: 'C', domain: 'mystery')
140
+ expect(engine.by_domain(domain: 'sci-fi').size).to eq(2)
141
+ end
142
+ end
143
+
144
+ describe '#most_coherent' do
145
+ it 'returns narratives sorted by coherence descending' do
146
+ engine.create_narrative(title: 'Low')
147
+ high = engine.create_narrative(title: 'High')
148
+ 5.times { high.add_event(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(content: 'x', event_type: :action)) }
149
+ result = engine.most_coherent(limit: 2)
150
+ expect(result.first).to eq(high)
151
+ end
152
+
153
+ it 'respects the limit' do
154
+ 3.times { |i| engine.create_narrative(title: "N#{i}") }
155
+ expect(engine.most_coherent(limit: 2).size).to be <= 2
156
+ end
157
+ end
158
+
159
+ describe '#decay_all' do
160
+ it 'reduces coherence of all narratives' do
161
+ n = engine.create_narrative(title: 'Decay Test')
162
+ before = n.coherence
163
+ engine.decay_all
164
+ expect(n.coherence).to be < before
165
+ end
166
+ end
167
+
168
+ describe '#to_h' do
169
+ it 'includes count and narratives array' do
170
+ engine.create_narrative(title: 'One')
171
+ h = engine.to_h
172
+ expect(h[:count]).to eq(1)
173
+ expect(h[:narratives]).to be_an(Array)
174
+ end
175
+ end
176
+
177
+ describe 'EVENT_TYPES constant' do
178
+ it 'includes the five required types' do
179
+ expect(described_class::EVENT_TYPES).to include(:action, :discovery, :conflict, :resolution, :revelation)
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent do
4
+ let(:event) do
5
+ described_class.new(
6
+ content: 'The detective discovers the clue',
7
+ event_type: :discovery,
8
+ characters: %w[detective suspect],
9
+ causes: [],
10
+ domain: 'mystery'
11
+ )
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'assigns a uuid id' do
16
+ expect(event.id).to match(/\A[0-9a-f-]{36}\z/)
17
+ end
18
+
19
+ it 'stores content' do
20
+ expect(event.content).to eq('The detective discovers the clue')
21
+ end
22
+
23
+ it 'stores event_type' do
24
+ expect(event.event_type).to eq(:discovery)
25
+ end
26
+
27
+ it 'stores characters as array' do
28
+ expect(event.characters).to contain_exactly('detective', 'suspect')
29
+ end
30
+
31
+ it 'stores causes' do
32
+ expect(event.causes).to eq([])
33
+ end
34
+
35
+ it 'stores domain' do
36
+ expect(event.domain).to eq('mystery')
37
+ end
38
+
39
+ it 'sets timestamp to now' do
40
+ expect(event.timestamp).to be_a(Time)
41
+ end
42
+
43
+ it 'accepts explicit id' do
44
+ custom = described_class.new(content: 'x', event_type: :action, id: 'custom-id')
45
+ expect(custom.id).to eq('custom-id')
46
+ end
47
+ end
48
+
49
+ describe '#to_h' do
50
+ it 'returns a hash with all fields' do
51
+ h = event.to_h
52
+ expect(h[:id]).to eq(event.id)
53
+ expect(h[:content]).to eq('The detective discovers the clue')
54
+ expect(h[:event_type]).to eq(:discovery)
55
+ expect(h[:characters]).to eq(%w[detective suspect])
56
+ expect(h[:causes]).to eq([])
57
+ expect(h[:domain]).to eq('mystery')
58
+ expect(h[:timestamp]).to be_a(Time)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::Narrative do
4
+ let(:narrative) { described_class.new(title: 'The Journey', domain: 'adventure') }
5
+
6
+ let(:event1) do
7
+ Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(
8
+ content: 'Hero leaves home',
9
+ event_type: :action,
10
+ characters: ['hero']
11
+ )
12
+ end
13
+
14
+ let(:event2) do
15
+ Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(
16
+ content: 'Hero discovers map',
17
+ event_type: :discovery,
18
+ characters: ['hero'],
19
+ causes: [event1.id]
20
+ )
21
+ end
22
+
23
+ describe '#initialize' do
24
+ it 'assigns id, title, domain' do
25
+ expect(narrative.id).to match(/\A[0-9a-f-]{36}\z/)
26
+ expect(narrative.title).to eq('The Journey')
27
+ expect(narrative.domain).to eq('adventure')
28
+ end
29
+
30
+ it 'starts at beginning arc stage' do
31
+ expect(narrative.arc_stage).to eq(:beginning)
32
+ end
33
+
34
+ it 'starts with default coherence' do
35
+ expect(narrative.coherence).to eq(described_class::DEFAULT_COHERENCE)
36
+ end
37
+
38
+ it 'starts with empty events, characters, themes' do
39
+ expect(narrative.events).to be_empty
40
+ expect(narrative.characters).to be_empty
41
+ expect(narrative.themes).to be_empty
42
+ end
43
+ end
44
+
45
+ describe '#add_event' do
46
+ it 'appends the event' do
47
+ narrative.add_event(event1)
48
+ expect(narrative.events).to include(event1)
49
+ end
50
+
51
+ it 'collects unique characters' do
52
+ narrative.add_event(event1)
53
+ narrative.add_event(event2)
54
+ expect(narrative.characters).to contain_exactly('hero')
55
+ end
56
+
57
+ it 'boosts coherence on each event' do
58
+ initial = narrative.coherence
59
+ narrative.add_event(event1)
60
+ expect(narrative.coherence).to be > initial
61
+ end
62
+
63
+ it 'does not exceed coherence ceiling' do
64
+ 15.times { narrative.add_event(event1) }
65
+ expect(narrative.coherence).to be <= described_class::COHERENCE_CEILING
66
+ end
67
+
68
+ it 'updates last_updated_at' do
69
+ before = narrative.last_updated_at
70
+ sleep(0.01)
71
+ narrative.add_event(event1)
72
+ expect(narrative.last_updated_at).to be >= before
73
+ end
74
+ end
75
+
76
+ describe '#add_theme' do
77
+ it 'adds a theme' do
78
+ narrative.add_theme('redemption')
79
+ expect(narrative.themes).to include('redemption')
80
+ end
81
+
82
+ it 'does not add duplicate themes' do
83
+ narrative.add_theme('redemption')
84
+ narrative.add_theme('redemption')
85
+ expect(narrative.themes.count('redemption')).to eq(1)
86
+ end
87
+ end
88
+
89
+ describe '#advance_arc!' do
90
+ it 'advances to next arc stage' do
91
+ narrative.advance_arc!
92
+ expect(narrative.arc_stage).to eq(:rising_action)
93
+ end
94
+
95
+ it 'does not advance past resolution' do
96
+ 5.times { narrative.advance_arc! }
97
+ expect(narrative.arc_stage).to eq(:resolution)
98
+ narrative.advance_arc!
99
+ expect(narrative.arc_stage).to eq(:resolution)
100
+ end
101
+ end
102
+
103
+ describe '#causal_chain' do
104
+ it 'returns empty with no causes' do
105
+ narrative.add_event(event1)
106
+ expect(narrative.causal_chain).to be_empty
107
+ end
108
+
109
+ it 'returns cause->effect links' do
110
+ narrative.add_event(event1)
111
+ narrative.add_event(event2)
112
+ chain = narrative.causal_chain
113
+ expect(chain.size).to eq(1)
114
+ expect(chain.first[:cause][:id]).to eq(event1.id)
115
+ expect(chain.first[:effect][:id]).to eq(event2.id)
116
+ end
117
+ end
118
+
119
+ describe '#coherence_label' do
120
+ it 'returns :developing for default coherence' do
121
+ expect(narrative.coherence_label).to eq(:developing)
122
+ end
123
+
124
+ it 'returns :compelling for high coherence' do
125
+ 10.times { narrative.add_event(event1) }
126
+ expect(narrative.coherence_label).to eq(:compelling)
127
+ end
128
+ end
129
+
130
+ describe '#complete?' do
131
+ it 'returns false at start' do
132
+ expect(narrative.complete?).to be false
133
+ end
134
+
135
+ it 'returns true at resolution' do
136
+ 5.times { narrative.advance_arc! }
137
+ expect(narrative.complete?).to be true
138
+ end
139
+ end
140
+
141
+ describe '#decay_coherence' do
142
+ it 'reduces coherence by DECAY_RATE' do
143
+ before = narrative.coherence
144
+ narrative.decay_coherence
145
+ expect(narrative.coherence).to be_within(0.001).of(before - described_class::DECAY_RATE)
146
+ end
147
+
148
+ it 'does not go below coherence floor' do
149
+ 200.times { narrative.decay_coherence }
150
+ expect(narrative.coherence).to eq(described_class::COHERENCE_FLOOR)
151
+ end
152
+ end
153
+
154
+ describe '#to_h' do
155
+ it 'includes all expected keys' do
156
+ h = narrative.to_h
157
+ expect(h).to include(:id, :title, :domain, :events, :characters, :themes,
158
+ :arc_stage, :coherence, :coherence_label, :complete,
159
+ :created_at, :last_updated_at)
160
+ end
161
+ end
162
+
163
+ describe 'ARC_STAGES constant' do
164
+ it 'contains the five expected stages in order' do
165
+ expect(described_class::ARC_STAGES).to eq(%i[beginning rising_action climax falling_action resolution])
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/language/narrative_reasoning/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Runners::NarrativeReasoning do
6
+ let(:client) { Legion::Extensions::Agentic::Language::NarrativeReasoning::Client.new }
7
+
8
+ describe '#create_narrative' do
9
+ it 'returns success with narrative_id and title' do
10
+ result = client.create_narrative(title: 'My Story', domain: 'fantasy')
11
+ expect(result[:success]).to be true
12
+ expect(result[:narrative_id]).to match(/\A[0-9a-f-]{36}\z/)
13
+ expect(result[:title]).to eq('My Story')
14
+ expect(result[:arc_stage]).to eq(:beginning)
15
+ end
16
+ end
17
+
18
+ describe '#add_narrative_event' do
19
+ let(:narrative_id) { client.create_narrative(title: 'Event Test')[:narrative_id] }
20
+
21
+ it 'adds an event and returns event_id' do
22
+ result = client.add_narrative_event(
23
+ narrative_id: narrative_id,
24
+ content: 'Hero acts',
25
+ event_type: :action,
26
+ characters: ['hero']
27
+ )
28
+ expect(result[:success]).to be true
29
+ expect(result[:event_id]).to match(/\A[0-9a-f-]{36}\z/)
30
+ end
31
+
32
+ it 'returns error for unknown narrative' do
33
+ result = client.add_narrative_event(narrative_id: 'bad', content: 'x', event_type: :action)
34
+ expect(result[:success]).to be false
35
+ expect(result[:error]).to eq(:narrative_not_found)
36
+ end
37
+
38
+ it 'returns error for invalid event_type' do
39
+ result = client.add_narrative_event(narrative_id: narrative_id, content: 'x', event_type: :bogus)
40
+ expect(result[:success]).to be false
41
+ expect(result[:error]).to eq(:invalid_event_type)
42
+ end
43
+ end
44
+
45
+ describe '#add_narrative_theme' do
46
+ let(:narrative_id) { client.create_narrative(title: 'Theme Test')[:narrative_id] }
47
+
48
+ it 'adds a theme successfully' do
49
+ result = client.add_narrative_theme(narrative_id: narrative_id, theme: 'courage')
50
+ expect(result[:success]).to be true
51
+ expect(result[:theme]).to eq('courage')
52
+ end
53
+
54
+ it 'returns error for unknown narrative' do
55
+ result = client.add_narrative_theme(narrative_id: 'nope', theme: 'x')
56
+ expect(result[:success]).to be false
57
+ expect(result[:error]).to eq(:narrative_not_found)
58
+ end
59
+ end
60
+
61
+ describe '#advance_narrative_arc' do
62
+ let(:narrative_id) { client.create_narrative(title: 'Arc Test')[:narrative_id] }
63
+
64
+ it 'advances to next arc stage' do
65
+ result = client.advance_narrative_arc(narrative_id: narrative_id)
66
+ expect(result[:success]).to be true
67
+ expect(result[:arc_stage]).to eq(:rising_action)
68
+ end
69
+
70
+ it 'returns error for unknown narrative' do
71
+ result = client.advance_narrative_arc(narrative_id: 'bad')
72
+ expect(result[:success]).to be false
73
+ expect(result[:error]).to eq(:narrative_not_found)
74
+ end
75
+ end
76
+
77
+ describe '#trace_narrative_causes' do
78
+ let(:narrative_id) { client.create_narrative(title: 'Causal Test')[:narrative_id] }
79
+
80
+ it 'returns empty chain with no causes' do
81
+ client.add_narrative_event(narrative_id: narrative_id, content: 'start', event_type: :action)
82
+ result = client.trace_narrative_causes(narrative_id: narrative_id)
83
+ expect(result[:success]).to be true
84
+ expect(result[:chain]).to eq([])
85
+ expect(result[:link_count]).to eq(0)
86
+ end
87
+
88
+ it 'returns causal links' do
89
+ evt = client.add_narrative_event(narrative_id: narrative_id, content: 'cause', event_type: :action)
90
+ client.add_narrative_event(
91
+ narrative_id: narrative_id,
92
+ content: 'effect',
93
+ event_type: :resolution,
94
+ causes: [evt[:event_id]]
95
+ )
96
+ result = client.trace_narrative_causes(narrative_id: narrative_id)
97
+ expect(result[:link_count]).to eq(1)
98
+ end
99
+
100
+ it 'returns error for unknown narrative' do
101
+ result = client.trace_narrative_causes(narrative_id: 'nope')
102
+ expect(result[:success]).to be false
103
+ end
104
+ end
105
+
106
+ describe '#complete_narratives' do
107
+ it 'returns empty list when none are complete' do
108
+ client.create_narrative(title: 'Incomplete')
109
+ result = client.complete_narratives
110
+ expect(result[:success]).to be true
111
+ expect(result[:count]).to eq(0)
112
+ end
113
+
114
+ it 'includes narratives that reached resolution' do
115
+ nid = client.create_narrative(title: 'Done')[:narrative_id]
116
+ 5.times { client.advance_narrative_arc(narrative_id: nid) }
117
+ result = client.complete_narratives
118
+ expect(result[:count]).to eq(1)
119
+ end
120
+ end
121
+
122
+ describe '#domain_narratives' do
123
+ it 'filters by domain' do
124
+ client.create_narrative(title: 'A', domain: 'sci-fi')
125
+ client.create_narrative(title: 'B', domain: 'sci-fi')
126
+ client.create_narrative(title: 'C', domain: 'horror')
127
+ result = client.domain_narratives(domain: 'sci-fi')
128
+ expect(result[:success]).to be true
129
+ expect(result[:count]).to eq(2)
130
+ end
131
+ end
132
+
133
+ describe '#most_coherent_narratives' do
134
+ it 'returns up to limit narratives' do
135
+ 3.times { |i| client.create_narrative(title: "N#{i}") }
136
+ result = client.most_coherent_narratives(limit: 2)
137
+ expect(result[:success]).to be true
138
+ expect(result[:narratives].size).to be <= 2
139
+ end
140
+
141
+ it 'clamps limit to minimum 1' do
142
+ client.create_narrative(title: 'Solo')
143
+ result = client.most_coherent_narratives(limit: 0)
144
+ expect(result[:narratives].size).to be >= 1
145
+ end
146
+ end
147
+
148
+ describe '#update_narrative_reasoning' do
149
+ it 'returns success with count of updated narratives' do
150
+ client.create_narrative(title: 'A')
151
+ client.create_narrative(title: 'B')
152
+ result = client.update_narrative_reasoning
153
+ expect(result[:success]).to be true
154
+ expect(result[:narratives_updated]).to eq(2)
155
+ end
156
+ end
157
+
158
+ describe '#narrative_reasoning_stats' do
159
+ it 'returns stats hash' do
160
+ client.create_narrative(title: 'Stats Test')
161
+ result = client.narrative_reasoning_stats
162
+ expect(result[:success]).to be true
163
+ expect(result[:total_narratives]).to eq(1)
164
+ expect(result[:total_events]).to be_a(Integer)
165
+ expect(result[:complete_narratives]).to be_a(Integer)
166
+ end
167
+
168
+ it 'reports top_coherence_label' do
169
+ client.create_narrative(title: 'Some Narrative')
170
+ result = client.narrative_reasoning_stats
171
+ expect(%i[compelling coherent developing fragmented incoherent]).to include(result[:top_coherence_label])
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'initializes with a default journal' do
7
+ expect(client.journal).to be_a(Legion::Extensions::Agentic::Language::Narrator::Helpers::Journal)
8
+ end
9
+
10
+ it 'accepts an injected journal' do
11
+ custom = Legion::Extensions::Agentic::Language::Narrator::Helpers::Journal.new
12
+ client = described_class.new(journal: custom)
13
+ expect(client.journal).to be(custom)
14
+ end
15
+
16
+ it 'includes the Narrator runner' do
17
+ expect(client).to respond_to(:narrate)
18
+ expect(client).to respond_to(:recent_entries)
19
+ expect(client).to respond_to(:entries_since)
20
+ expect(client).to respond_to(:mood_history)
21
+ expect(client).to respond_to(:current_narrative)
22
+ expect(client).to respond_to(:narrator_stats)
23
+ end
24
+ end