lex-agentic-self 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 (249) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-self.gemspec +31 -0
  7. data/lib/legion/extensions/agentic/self/agency/client.rb +21 -0
  8. data/lib/legion/extensions/agentic/self/agency/helpers/constants.rb +77 -0
  9. data/lib/legion/extensions/agentic/self/agency/helpers/efficacy_model.rb +136 -0
  10. data/lib/legion/extensions/agentic/self/agency/helpers/outcome_event.rb +52 -0
  11. data/lib/legion/extensions/agentic/self/agency/runners/agency.rb +117 -0
  12. data/lib/legion/extensions/agentic/self/agency/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/self/agency.rb +19 -0
  14. data/lib/legion/extensions/agentic/self/anchor/client.rb +15 -0
  15. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor.rb +92 -0
  16. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor_engine.rb +123 -0
  17. data/lib/legion/extensions/agentic/self/anchor/helpers/chain.rb +93 -0
  18. data/lib/legion/extensions/agentic/self/anchor/helpers/constants.rb +46 -0
  19. data/lib/legion/extensions/agentic/self/anchor/runners/cognitive_anchor.rb +70 -0
  20. data/lib/legion/extensions/agentic/self/anchor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/self/anchor.rb +22 -0
  22. data/lib/legion/extensions/agentic/self/anosognosia/client.rb +28 -0
  23. data/lib/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine.rb +153 -0
  24. data/lib/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit.rb +71 -0
  25. data/lib/legion/extensions/agentic/self/anosognosia/helpers/constants.rb +29 -0
  26. data/lib/legion/extensions/agentic/self/anosognosia/runners/anosognosia.rb +98 -0
  27. data/lib/legion/extensions/agentic/self/anosognosia/version.rb +13 -0
  28. data/lib/legion/extensions/agentic/self/anosognosia.rb +19 -0
  29. data/lib/legion/extensions/agentic/self/architecture/client.rb +19 -0
  30. data/lib/legion/extensions/agentic/self/architecture/helpers/architecture_engine.rb +167 -0
  31. data/lib/legion/extensions/agentic/self/architecture/helpers/connection.rb +57 -0
  32. data/lib/legion/extensions/agentic/self/architecture/helpers/constants.rb +37 -0
  33. data/lib/legion/extensions/agentic/self/architecture/helpers/subsystem.rb +80 -0
  34. data/lib/legion/extensions/agentic/self/architecture/runners/cognitive_architecture.rb +125 -0
  35. data/lib/legion/extensions/agentic/self/architecture/version.rb +13 -0
  36. data/lib/legion/extensions/agentic/self/architecture.rb +20 -0
  37. data/lib/legion/extensions/agentic/self/default_mode_network/actors/idle.rb +45 -0
  38. data/lib/legion/extensions/agentic/self/default_mode_network/client.rb +28 -0
  39. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine.rb +221 -0
  41. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought.rb +60 -0
  42. data/lib/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network.rb +122 -0
  43. data/lib/legion/extensions/agentic/self/default_mode_network/version.rb +13 -0
  44. data/lib/legion/extensions/agentic/self/default_mode_network.rb +20 -0
  45. data/lib/legion/extensions/agentic/self/fingerprint/client.rb +28 -0
  46. data/lib/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait.rb +73 -0
  47. data/lib/legion/extensions/agentic/self/fingerprint/helpers/constants.rb +60 -0
  48. data/lib/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine.rb +169 -0
  49. data/lib/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint.rb +86 -0
  50. data/lib/legion/extensions/agentic/self/fingerprint/version.rb +13 -0
  51. data/lib/legion/extensions/agentic/self/fingerprint.rb +19 -0
  52. data/lib/legion/extensions/agentic/self/identity/actors/credential_refresh.rb +49 -0
  53. data/lib/legion/extensions/agentic/self/identity/actors/orphan_check.rb +52 -0
  54. data/lib/legion/extensions/agentic/self/identity/client.rb +27 -0
  55. data/lib/legion/extensions/agentic/self/identity/helpers/dimensions.rb +75 -0
  56. data/lib/legion/extensions/agentic/self/identity/helpers/fingerprint.rb +170 -0
  57. data/lib/legion/extensions/agentic/self/identity/helpers/graph_client.rb +29 -0
  58. data/lib/legion/extensions/agentic/self/identity/helpers/graph_token.rb +36 -0
  59. data/lib/legion/extensions/agentic/self/identity/helpers/token_cache.rb +59 -0
  60. data/lib/legion/extensions/agentic/self/identity/helpers/vault_secrets.rb +80 -0
  61. data/lib/legion/extensions/agentic/self/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
  62. data/lib/legion/extensions/agentic/self/identity/runners/entra.rb +402 -0
  63. data/lib/legion/extensions/agentic/self/identity/runners/identity.rb +90 -0
  64. data/lib/legion/extensions/agentic/self/identity/version.rb +13 -0
  65. data/lib/legion/extensions/agentic/self/identity.rb +28 -0
  66. data/lib/legion/extensions/agentic/self/metacognition/client.rb +27 -0
  67. data/lib/legion/extensions/agentic/self/metacognition/helpers/constants.rb +377 -0
  68. data/lib/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge.rb +85 -0
  69. data/lib/legion/extensions/agentic/self/metacognition/helpers/registry_store.rb +70 -0
  70. data/lib/legion/extensions/agentic/self/metacognition/helpers/self_model.rb +160 -0
  71. data/lib/legion/extensions/agentic/self/metacognition/helpers/snapshot_store.rb +82 -0
  72. data/lib/legion/extensions/agentic/self/metacognition/runners/metacognition.rb +116 -0
  73. data/lib/legion/extensions/agentic/self/metacognition/runners/registry.rb +180 -0
  74. data/lib/legion/extensions/agentic/self/metacognition/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/self/metacognition.rb +22 -0
  76. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/client.rb +25 -0
  77. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker.rb +96 -0
  78. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants.rb +47 -0
  79. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine.rb +141 -0
  80. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment.rb +79 -0
  81. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring.rb +151 -0
  82. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/version.rb +13 -0
  83. data/lib/legion/extensions/agentic/self/metacognitive_monitoring.rb +20 -0
  84. data/lib/legion/extensions/agentic/self/narrative_arc/client.rb +29 -0
  85. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc.rb +137 -0
  86. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine.rb +119 -0
  87. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/beat_event.rb +59 -0
  88. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/constants.rb +66 -0
  89. data/lib/legion/extensions/agentic/self/narrative_arc/runners/narrative.rb +101 -0
  90. data/lib/legion/extensions/agentic/self/narrative_arc/version.rb +13 -0
  91. data/lib/legion/extensions/agentic/self/narrative_arc.rb +20 -0
  92. data/lib/legion/extensions/agentic/self/narrative_identity/actors/narrative_decay.rb +45 -0
  93. data/lib/legion/extensions/agentic/self/narrative_identity/client.rb +22 -0
  94. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/chapter.rb +48 -0
  95. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/constants.rb +62 -0
  96. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/episode.rb +67 -0
  97. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine.rb +187 -0
  98. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/theme.rb +50 -0
  99. data/lib/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity.rb +158 -0
  100. data/lib/legion/extensions/agentic/self/narrative_identity/version.rb +13 -0
  101. data/lib/legion/extensions/agentic/self/narrative_identity.rb +21 -0
  102. data/lib/legion/extensions/agentic/self/narrative_self/client.rb +27 -0
  103. data/lib/legion/extensions/agentic/self/narrative_self/helpers/autobiography.rb +187 -0
  104. data/lib/legion/extensions/agentic/self/narrative_self/helpers/constants.rb +42 -0
  105. data/lib/legion/extensions/agentic/self/narrative_self/helpers/episode.rb +81 -0
  106. data/lib/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread.rb +65 -0
  107. data/lib/legion/extensions/agentic/self/narrative_self/runners/narrative_self.rb +86 -0
  108. data/lib/legion/extensions/agentic/self/narrative_self/version.rb +13 -0
  109. data/lib/legion/extensions/agentic/self/narrative_self.rb +20 -0
  110. data/lib/legion/extensions/agentic/self/personality/client.rb +21 -0
  111. data/lib/legion/extensions/agentic/self/personality/helpers/constants.rb +84 -0
  112. data/lib/legion/extensions/agentic/self/personality/helpers/personality_store.rb +126 -0
  113. data/lib/legion/extensions/agentic/self/personality/helpers/trait_model.rb +147 -0
  114. data/lib/legion/extensions/agentic/self/personality/runners/personality.rb +102 -0
  115. data/lib/legion/extensions/agentic/self/personality/version.rb +13 -0
  116. data/lib/legion/extensions/agentic/self/personality.rb +19 -0
  117. data/lib/legion/extensions/agentic/self/reflection/client.rb +27 -0
  118. data/lib/legion/extensions/agentic/self/reflection/helpers/constants.rb +66 -0
  119. data/lib/legion/extensions/agentic/self/reflection/helpers/llm_enhancer.rb +166 -0
  120. data/lib/legion/extensions/agentic/self/reflection/helpers/monitors.rb +186 -0
  121. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection.rb +54 -0
  122. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection_store.rb +99 -0
  123. data/lib/legion/extensions/agentic/self/reflection/runners/reflection.rb +199 -0
  124. data/lib/legion/extensions/agentic/self/reflection/version.rb +13 -0
  125. data/lib/legion/extensions/agentic/self/reflection.rb +21 -0
  126. data/lib/legion/extensions/agentic/self/self_model/client.rb +19 -0
  127. data/lib/legion/extensions/agentic/self/self_model/helpers/capability.rb +93 -0
  128. data/lib/legion/extensions/agentic/self/self_model/helpers/constants.rb +46 -0
  129. data/lib/legion/extensions/agentic/self/self_model/helpers/knowledge_domain.rb +82 -0
  130. data/lib/legion/extensions/agentic/self/self_model/helpers/self_model.rb +150 -0
  131. data/lib/legion/extensions/agentic/self/self_model/runners/self_model.rb +82 -0
  132. data/lib/legion/extensions/agentic/self/self_model/version.rb +13 -0
  133. data/lib/legion/extensions/agentic/self/self_model.rb +21 -0
  134. data/lib/legion/extensions/agentic/self/self_talk/actors/volume_decay.rb +45 -0
  135. data/lib/legion/extensions/agentic/self/self_talk/client.rb +30 -0
  136. data/lib/legion/extensions/agentic/self/self_talk/helpers/constants.rb +63 -0
  137. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue.rb +114 -0
  138. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn.rb +43 -0
  139. data/lib/legion/extensions/agentic/self/self_talk/helpers/inner_voice.rb +77 -0
  140. data/lib/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer.rb +135 -0
  141. data/lib/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine.rb +160 -0
  142. data/lib/legion/extensions/agentic/self/self_talk/runners/self_talk.rb +172 -0
  143. data/lib/legion/extensions/agentic/self/self_talk/version.rb +13 -0
  144. data/lib/legion/extensions/agentic/self/self_talk.rb +22 -0
  145. data/lib/legion/extensions/agentic/self/version.rb +11 -0
  146. data/lib/legion/extensions/agentic/self.rb +33 -0
  147. data/spec/legion/extensions/agentic/self/agency/client_spec.rb +67 -0
  148. data/spec/legion/extensions/agentic/self/agency/helpers/constants_spec.rb +73 -0
  149. data/spec/legion/extensions/agentic/self/agency/helpers/efficacy_model_spec.rb +190 -0
  150. data/spec/legion/extensions/agentic/self/agency/helpers/outcome_event_spec.rb +85 -0
  151. data/spec/legion/extensions/agentic/self/agency/runners/agency_spec.rb +132 -0
  152. data/spec/legion/extensions/agentic/self/anchor/client_spec.rb +30 -0
  153. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_engine_spec.rb +109 -0
  154. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_spec.rb +124 -0
  155. data/spec/legion/extensions/agentic/self/anchor/helpers/chain_spec.rb +106 -0
  156. data/spec/legion/extensions/agentic/self/anchor/helpers/constants_spec.rb +53 -0
  157. data/spec/legion/extensions/agentic/self/anchor/runners/cognitive_anchor_spec.rb +70 -0
  158. data/spec/legion/extensions/agentic/self/anosognosia/anosognosia_spec.rb +15 -0
  159. data/spec/legion/extensions/agentic/self/anosognosia/client_spec.rb +50 -0
  160. data/spec/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine_spec.rb +266 -0
  161. data/spec/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit_spec.rb +150 -0
  162. data/spec/legion/extensions/agentic/self/anosognosia/helpers/constants_spec.rb +58 -0
  163. data/spec/legion/extensions/agentic/self/anosognosia/runners/anosognosia_spec.rb +225 -0
  164. data/spec/legion/extensions/agentic/self/architecture/client_spec.rb +51 -0
  165. data/spec/legion/extensions/agentic/self/architecture/helpers/architecture_engine_spec.rb +321 -0
  166. data/spec/legion/extensions/agentic/self/architecture/helpers/connection_spec.rb +118 -0
  167. data/spec/legion/extensions/agentic/self/architecture/helpers/subsystem_spec.rb +189 -0
  168. data/spec/legion/extensions/agentic/self/architecture/runners/cognitive_architecture_spec.rb +181 -0
  169. data/spec/legion/extensions/agentic/self/default_mode_network/client_spec.rb +69 -0
  170. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/constants_spec.rb +76 -0
  171. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine_spec.rb +321 -0
  172. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought_spec.rb +145 -0
  173. data/spec/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network_spec.rb +269 -0
  174. data/spec/legion/extensions/agentic/self/fingerprint/client_spec.rb +54 -0
  175. data/spec/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait_spec.rb +180 -0
  176. data/spec/legion/extensions/agentic/self/fingerprint/helpers/constants_spec.rb +108 -0
  177. data/spec/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine_spec.rb +318 -0
  178. data/spec/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint_spec.rb +232 -0
  179. data/spec/legion/extensions/agentic/self/identity/actors/orphan_check_spec.rb +104 -0
  180. data/spec/legion/extensions/agentic/self/identity/client_spec.rb +32 -0
  181. data/spec/legion/extensions/agentic/self/identity/helpers/dimensions_spec.rb +51 -0
  182. data/spec/legion/extensions/agentic/self/identity/helpers/fingerprint_spec.rb +66 -0
  183. data/spec/legion/extensions/agentic/self/identity/helpers/graph_client_spec.rb +19 -0
  184. data/spec/legion/extensions/agentic/self/identity/helpers/graph_token_spec.rb +31 -0
  185. data/spec/legion/extensions/agentic/self/identity/helpers/token_cache_spec.rb +50 -0
  186. data/spec/legion/extensions/agentic/self/identity/local_persistence_spec.rb +329 -0
  187. data/spec/legion/extensions/agentic/self/identity/runners/entra_spec.rb +655 -0
  188. data/spec/legion/extensions/agentic/self/identity/runners/identity_spec.rb +61 -0
  189. data/spec/legion/extensions/agentic/self/metacognition/client_spec.rb +20 -0
  190. data/spec/legion/extensions/agentic/self/metacognition/helpers/constants_spec.rb +31 -0
  191. data/spec/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge_spec.rb +102 -0
  192. data/spec/legion/extensions/agentic/self/metacognition/helpers/registry_store_spec.rb +227 -0
  193. data/spec/legion/extensions/agentic/self/metacognition/helpers/self_model_spec.rb +117 -0
  194. data/spec/legion/extensions/agentic/self/metacognition/helpers/snapshot_store_spec.rb +128 -0
  195. data/spec/legion/extensions/agentic/self/metacognition/runners/metacognition_spec.rb +110 -0
  196. data/spec/legion/extensions/agentic/self/metacognition/runners/registry_spec.rb +281 -0
  197. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/client_spec.rb +59 -0
  198. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker_spec.rb +143 -0
  199. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants_spec.rb +91 -0
  200. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine_spec.rb +198 -0
  201. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment_spec.rb +172 -0
  202. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring_spec.rb +244 -0
  203. data/spec/legion/extensions/agentic/self/narrative_arc/client_spec.rb +22 -0
  204. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine_spec.rb +183 -0
  205. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_spec.rb +177 -0
  206. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/beat_event_spec.rb +96 -0
  207. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/constants_spec.rb +75 -0
  208. data/spec/legion/extensions/agentic/self/narrative_arc/runners/narrative_spec.rb +142 -0
  209. data/spec/legion/extensions/agentic/self/narrative_identity/client_spec.rb +69 -0
  210. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/chapter_spec.rb +85 -0
  211. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/constants_spec.rb +83 -0
  212. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/episode_spec.rb +180 -0
  213. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine_spec.rb +307 -0
  214. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/theme_spec.rb +107 -0
  215. data/spec/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity_spec.rb +240 -0
  216. data/spec/legion/extensions/agentic/self/narrative_self/client_spec.rb +67 -0
  217. data/spec/legion/extensions/agentic/self/narrative_self/helpers/autobiography_spec.rb +155 -0
  218. data/spec/legion/extensions/agentic/self/narrative_self/helpers/constants_spec.rb +28 -0
  219. data/spec/legion/extensions/agentic/self/narrative_self/helpers/episode_spec.rb +144 -0
  220. data/spec/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread_spec.rb +87 -0
  221. data/spec/legion/extensions/agentic/self/narrative_self/runners/narrative_self_spec.rb +118 -0
  222. data/spec/legion/extensions/agentic/self/personality/client_spec.rb +20 -0
  223. data/spec/legion/extensions/agentic/self/personality/helpers/constants_spec.rb +41 -0
  224. data/spec/legion/extensions/agentic/self/personality/helpers/personality_store_spec.rb +66 -0
  225. data/spec/legion/extensions/agentic/self/personality/helpers/trait_model_spec.rb +148 -0
  226. data/spec/legion/extensions/agentic/self/personality/runners/personality_spec.rb +67 -0
  227. data/spec/legion/extensions/agentic/self/reflection/client_spec.rb +24 -0
  228. data/spec/legion/extensions/agentic/self/reflection/helpers/llm_enhancer_spec.rb +191 -0
  229. data/spec/legion/extensions/agentic/self/reflection/helpers/monitors_spec.rb +120 -0
  230. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_spec.rb +49 -0
  231. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_store_spec.rb +93 -0
  232. data/spec/legion/extensions/agentic/self/reflection/runners/reflection_spec.rb +204 -0
  233. data/spec/legion/extensions/agentic/self/self_model/client_spec.rb +55 -0
  234. data/spec/legion/extensions/agentic/self/self_model/helpers/capability_spec.rb +160 -0
  235. data/spec/legion/extensions/agentic/self/self_model/helpers/knowledge_domain_spec.rb +128 -0
  236. data/spec/legion/extensions/agentic/self/self_model/helpers/self_model_spec.rb +238 -0
  237. data/spec/legion/extensions/agentic/self/self_model/runners/self_model_spec.rb +143 -0
  238. data/spec/legion/extensions/agentic/self/self_talk/actors/volume_decay_spec.rb +46 -0
  239. data/spec/legion/extensions/agentic/self/self_talk/client_spec.rb +26 -0
  240. data/spec/legion/extensions/agentic/self/self_talk/helpers/constants_spec.rb +110 -0
  241. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_spec.rb +191 -0
  242. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn_spec.rb +78 -0
  243. data/spec/legion/extensions/agentic/self/self_talk/helpers/inner_voice_spec.rb +172 -0
  244. data/spec/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer_spec.rb +206 -0
  245. data/spec/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine_spec.rb +239 -0
  246. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_llm_spec.rb +169 -0
  247. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_spec.rb +196 -0
  248. data/spec/spec_helper.rb +46 -0
  249. metadata +347 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::ArcEngine do
4
+ let(:engine) { described_class.new }
5
+
6
+ def add_beat_to_arc(arc_id, beat_type: :rising_action, intensity: 0.5)
7
+ engine.add_beat(
8
+ arc_id: arc_id,
9
+ content: "Beat content for #{beat_type}",
10
+ beat_type: beat_type,
11
+ intensity: intensity
12
+ )
13
+ end
14
+
15
+ describe '#create_arc' do
16
+ it 'creates and returns an arc' do
17
+ arc = engine.create_arc(title: 'Test arc')
18
+ expect(arc).to be_a(Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Arc)
19
+ end
20
+
21
+ it 'stores the arc internally' do
22
+ arc = engine.create_arc(title: 'Stored arc')
23
+ expect(engine.get_arc(arc.arc_id)).to eq(arc)
24
+ end
25
+
26
+ it 'returns nil when engine is at capacity' do
27
+ stub_const(
28
+ 'Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Constants::MAX_ARCS',
29
+ 0
30
+ )
31
+ expect(engine.create_arc(title: 'overflow')).to be_nil
32
+ end
33
+ end
34
+
35
+ describe '#add_beat' do
36
+ let(:arc) { engine.create_arc(title: 'Arc for beats') }
37
+
38
+ it 'adds a beat successfully' do
39
+ result = add_beat_to_arc(arc.arc_id)
40
+ expect(result[:success]).to be true
41
+ expect(result[:beat_id]).to match(/\A[0-9a-f-]{36}\z/)
42
+ end
43
+
44
+ it 'returns arc_phase and tension_level in the result' do
45
+ result = add_beat_to_arc(arc.arc_id)
46
+ expect(result[:arc_phase]).to be_a(Symbol)
47
+ expect(result[:tension_level]).to be_a(Float)
48
+ end
49
+
50
+ it 'returns failure for unknown arc_id' do
51
+ result = engine.add_beat(arc_id: 'nonexistent', content: 'x')
52
+ expect(result[:success]).to be false
53
+ expect(result[:reason]).to eq(:arc_not_found)
54
+ end
55
+ end
56
+
57
+ describe '#get_arc' do
58
+ it 'returns nil for unknown arc_id' do
59
+ expect(engine.get_arc('missing')).to be_nil
60
+ end
61
+
62
+ it 'returns the arc object for a known id' do
63
+ arc = engine.create_arc(title: 'Known arc')
64
+ expect(engine.get_arc(arc.arc_id)).to eq(arc)
65
+ end
66
+ end
67
+
68
+ describe '#active_arcs' do
69
+ it 'returns arcs that are not complete' do
70
+ engine.create_arc(title: 'Active one')
71
+ engine.create_arc(title: 'Active two')
72
+ expect(engine.active_arcs.size).to eq(2)
73
+ end
74
+
75
+ it 'excludes completed arcs' do
76
+ arc = engine.create_arc(title: 'Will complete')
77
+ add_beat_to_arc(arc.arc_id, beat_type: :climax, intensity: 0.95)
78
+ add_beat_to_arc(arc.arc_id, beat_type: :resolution)
79
+ expect(engine.active_arcs).not_to include(arc)
80
+ end
81
+ end
82
+
83
+ describe '#completed_arcs' do
84
+ it 'returns only completed arcs' do
85
+ engine.create_arc(title: 'Still active')
86
+ arc = engine.create_arc(title: 'Will complete')
87
+ add_beat_to_arc(arc.arc_id, beat_type: :climax, intensity: 0.95)
88
+ add_beat_to_arc(arc.arc_id, beat_type: :resolution)
89
+
90
+ completed = engine.completed_arcs
91
+ expect(completed.size).to eq(1)
92
+ expect(completed.first.arc_id).to eq(arc.arc_id)
93
+ end
94
+ end
95
+
96
+ describe '#most_dramatic_arc' do
97
+ it 'returns nil when no arcs exist' do
98
+ expect(engine.most_dramatic_arc).to be_nil
99
+ end
100
+
101
+ it 'returns the arc with the highest dramatic score' do
102
+ arc_low = engine.create_arc(title: 'Low drama')
103
+ arc_high = engine.create_arc(title: 'High drama')
104
+ add_beat_to_arc(arc_high.arc_id, beat_type: :climax, intensity: 0.95)
105
+ add_beat_to_arc(arc_high.arc_id, beat_type: :complication, intensity: 0.8)
106
+
107
+ expect(engine.most_dramatic_arc.arc_id).to eq(arc_high.arc_id)
108
+ expect(engine.most_dramatic_arc.arc_id).not_to eq(arc_low.arc_id)
109
+ end
110
+ end
111
+
112
+ describe '#tension_distribution' do
113
+ it 'returns empty hash when no arcs exist' do
114
+ expect(engine.tension_distribution).to eq({})
115
+ end
116
+
117
+ it 'categorizes arcs by tension label' do
118
+ engine.create_arc(title: 'Low tension')
119
+ dist = engine.tension_distribution
120
+ expect(dist).to be_a(Hash)
121
+ expect(dist.values.sum).to eq(1)
122
+ end
123
+ end
124
+
125
+ describe '#detect_narrative_patterns' do
126
+ it 'returns empty array with fewer than 2 arcs' do
127
+ engine.create_arc(title: 'Solo arc')
128
+ expect(engine.detect_narrative_patterns).to eq([])
129
+ end
130
+
131
+ it 'detects recurring_crisis pattern across arcs' do
132
+ 2.times do |i|
133
+ a = engine.create_arc(title: "Crisis arc #{i}")
134
+ add_beat_to_arc(a.arc_id, beat_type: :crisis)
135
+ end
136
+ expect(engine.detect_narrative_patterns).to include(:recurring_crisis)
137
+ end
138
+
139
+ it 'detects recurring_climax pattern across arcs' do
140
+ 2.times do |i|
141
+ a = engine.create_arc(title: "Climax arc #{i}")
142
+ add_beat_to_arc(a.arc_id, beat_type: :climax, intensity: 0.95)
143
+ end
144
+ expect(engine.detect_narrative_patterns).to include(:recurring_climax)
145
+ end
146
+
147
+ it 'detects unresolved_tension when an active arc is at climax level' do
148
+ a = engine.create_arc(title: 'High tension')
149
+ b = engine.create_arc(title: 'Normal')
150
+ 50.times { a.tension_rise! }
151
+ expect(engine.detect_narrative_patterns).to include(:unresolved_tension)
152
+ expect(b).to be_a(Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Arc)
153
+ end
154
+
155
+ it 'detects rapid_resolution pattern for arcs with <= 3 beats' do
156
+ arc = engine.create_arc(title: 'Fast resolve 1')
157
+ second = engine.create_arc(title: 'Fast resolve 2')
158
+ add_beat_to_arc(arc.arc_id, beat_type: :climax, intensity: 0.95)
159
+ add_beat_to_arc(arc.arc_id, beat_type: :resolution)
160
+ expect(engine.detect_narrative_patterns).to include(:rapid_resolution)
161
+ expect(second).to be_a(Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Arc)
162
+ end
163
+ end
164
+
165
+ describe '#arc_report' do
166
+ it 'returns a structured report' do
167
+ engine.create_arc(title: 'Report arc')
168
+ report = engine.arc_report
169
+ expect(report[:total_arcs]).to eq(1)
170
+ expect(report[:active]).to eq(1)
171
+ expect(report[:completed]).to eq(0)
172
+ expect(report[:patterns]).to be_an(Array)
173
+ expect(report[:tension_dist]).to be_a(Hash)
174
+ end
175
+
176
+ it 'includes most_dramatic in report' do
177
+ engine.create_arc(title: 'Dramatic arc')
178
+ report = engine.arc_report
179
+ expect(report[:most_dramatic]).to be_a(Hash)
180
+ expect(report[:most_dramatic]).to include(:arc_id)
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Arc do
4
+ let(:arc) { described_class.new(title: 'Debugging the anomaly', domain: :technical) }
5
+
6
+ let(:beat_rising) do
7
+ Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::BeatEvent.new(
8
+ content: 'First clue found', beat_type: :rising_action, intensity: 0.4
9
+ )
10
+ end
11
+
12
+ let(:beat_climax) do
13
+ Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::BeatEvent.new(
14
+ content: 'Root cause identified', beat_type: :climax, intensity: 0.95
15
+ )
16
+ end
17
+
18
+ let(:beat_resolution) do
19
+ Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::BeatEvent.new(
20
+ content: 'Fix applied', beat_type: :resolution, intensity: 0.3
21
+ )
22
+ end
23
+
24
+ describe '#initialize' do
25
+ it 'assigns a uuid arc_id' do
26
+ expect(arc.arc_id).to match(/\A[0-9a-f-]{36}\z/)
27
+ end
28
+
29
+ it 'starts in building phase' do
30
+ expect(arc.arc_phase).to eq(:building)
31
+ end
32
+
33
+ it 'starts with default tension' do
34
+ expect(arc.tension_level).to eq(Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Constants::DEFAULT_TENSION)
35
+ end
36
+
37
+ it 'starts with empty beats array' do
38
+ expect(arc.beats).to be_empty
39
+ end
40
+ end
41
+
42
+ describe '#add_beat!' do
43
+ it 'adds a beat and returns true' do
44
+ expect(arc.add_beat!(beat_rising)).to be true
45
+ expect(arc.beats.size).to eq(1)
46
+ end
47
+
48
+ it 'raises tension on rising_action beats' do
49
+ initial = arc.tension_level
50
+ arc.add_beat!(beat_rising)
51
+ expect(arc.tension_level).to be > initial
52
+ end
53
+
54
+ it 'transitions to peak phase on climax beat' do
55
+ arc.add_beat!(beat_climax)
56
+ expect(arc.arc_phase).to eq(:peak)
57
+ end
58
+
59
+ it 'transitions to complete phase on resolution beat after climax' do
60
+ arc.add_beat!(beat_climax)
61
+ arc.add_beat!(beat_resolution)
62
+ expect(arc.arc_phase).to eq(:complete)
63
+ end
64
+
65
+ it 'returns false when arc is complete' do
66
+ arc.add_beat!(beat_climax)
67
+ arc.add_beat!(beat_resolution)
68
+ expect(arc.add_beat!(beat_rising)).to be false
69
+ end
70
+ end
71
+
72
+ describe '#tension_rise!' do
73
+ it 'increases tension by the default rise amount' do
74
+ initial = arc.tension_level
75
+ arc.tension_rise!
76
+ expect(arc.tension_level).to be > initial
77
+ end
78
+
79
+ it 'clamps tension to 1.0' do
80
+ 50.times { arc.tension_rise! }
81
+ expect(arc.tension_level).to eq(1.0)
82
+ end
83
+
84
+ it 'accepts a custom amount' do
85
+ initial = arc.tension_level
86
+ arc.tension_rise!(0.5)
87
+ expect(arc.tension_level).to be_within(0.001).of(initial + 0.5)
88
+ end
89
+ end
90
+
91
+ describe '#tension_fall!' do
92
+ it 'decreases tension by the default fall amount' do
93
+ arc.tension_rise!(0.5)
94
+ initial = arc.tension_level
95
+ arc.tension_fall!
96
+ expect(arc.tension_level).to be < initial
97
+ end
98
+
99
+ it 'clamps tension to 0.0' do
100
+ 50.times { arc.tension_fall! }
101
+ expect(arc.tension_level).to eq(0.0)
102
+ end
103
+ end
104
+
105
+ describe '#climaxed?' do
106
+ it 'returns false for a fresh arc' do
107
+ expect(arc.climaxed?).to be false
108
+ end
109
+
110
+ it 'returns true when tension is at or above climax threshold' do
111
+ 50.times { arc.tension_rise! }
112
+ expect(arc.climaxed?).to be true
113
+ end
114
+
115
+ it 'returns true when arc is in peak phase' do
116
+ arc.add_beat!(beat_climax)
117
+ expect(arc.climaxed?).to be true
118
+ end
119
+ end
120
+
121
+ describe '#resolved?' do
122
+ it 'returns false before resolution' do
123
+ expect(arc.resolved?).to be false
124
+ end
125
+
126
+ it 'returns true after resolution beat' do
127
+ arc.add_beat!(beat_climax)
128
+ arc.add_beat!(beat_resolution)
129
+ expect(arc.resolved?).to be true
130
+ end
131
+ end
132
+
133
+ describe '#dramatic_score' do
134
+ it 'returns 0.0 for an empty arc' do
135
+ expect(arc.dramatic_score).to eq(0.0)
136
+ end
137
+
138
+ it 'returns a value between 0 and 1' do
139
+ arc.add_beat!(beat_rising)
140
+ arc.add_beat!(beat_climax)
141
+ expect(arc.dramatic_score).to be_between(0.0, 1.0)
142
+ end
143
+
144
+ it 'increases after adding a high-intensity beat' do
145
+ arc.add_beat!(beat_rising)
146
+ score_before = arc.dramatic_score
147
+ arc.add_beat!(beat_climax)
148
+ expect(arc.dramatic_score).to be >= score_before
149
+ end
150
+ end
151
+
152
+ describe '#to_h' do
153
+ it 'includes all required keys' do
154
+ h = arc.to_h
155
+ expect(h.keys).to include(:arc_id, :title, :domain, :arc_phase, :tension_level,
156
+ :beat_count, :dramatic_score, :tension_label, :drama_label,
157
+ :created_at, :resolved_at)
158
+ end
159
+
160
+ it 'reflects current beat count' do
161
+ arc.add_beat!(beat_rising)
162
+ expect(arc.to_h[:beat_count]).to eq(1)
163
+ end
164
+ end
165
+
166
+ describe '#advance_phase!' do
167
+ it 'returns the current phase' do
168
+ expect(arc.advance_phase!).to eq(:building)
169
+ end
170
+
171
+ it 'transitions to peak when tension hits climax threshold' do
172
+ 50.times { arc.tension_rise! }
173
+ arc.advance_phase!
174
+ expect(arc.arc_phase).to eq(:peak)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::BeatEvent do
4
+ let(:beat) { described_class.new(content: 'A key realization', beat_type: :rising_action) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid beat_id' do
8
+ expect(beat.beat_id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores content' do
12
+ expect(beat.content).to eq('A key realization')
13
+ end
14
+
15
+ it 'defaults intensity to 0.5' do
16
+ expect(beat.intensity).to eq(0.5)
17
+ end
18
+
19
+ it 'clamps intensity to 0..1 range' do
20
+ high = described_class.new(content: 'x', intensity: 5.0)
21
+ low = described_class.new(content: 'x', intensity: -2.0)
22
+ expect(high.intensity).to eq(1.0)
23
+ expect(low.intensity).to eq(0.0)
24
+ end
25
+
26
+ it 'clamps emotional_charge to -1..1 range' do
27
+ positive = described_class.new(content: 'x', emotional_charge: 2.0)
28
+ negative = described_class.new(content: 'x', emotional_charge: -3.0)
29
+ expect(positive.emotional_charge).to eq(1.0)
30
+ expect(negative.emotional_charge).to eq(-1.0)
31
+ end
32
+
33
+ it 'defaults to exposition when beat_type is invalid' do
34
+ beat = described_class.new(content: 'x', beat_type: :invalid_type)
35
+ expect(beat.beat_type).to eq(:exposition)
36
+ end
37
+
38
+ it 'accepts all valid beat types' do
39
+ Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Constants::BEAT_TYPES.each do |type|
40
+ b = described_class.new(content: 'x', beat_type: type)
41
+ expect(b.beat_type).to eq(type)
42
+ end
43
+ end
44
+
45
+ it 'records created_at timestamp' do
46
+ expect(beat.created_at).to be_a(Time)
47
+ end
48
+ end
49
+
50
+ describe '#to_h' do
51
+ it 'returns a hash with all expected keys' do
52
+ h = beat.to_h
53
+ expect(h.keys).to include(:beat_id, :content, :intensity, :beat_type, :domain,
54
+ :emotional_charge, :created_at)
55
+ end
56
+ end
57
+
58
+ describe '#climactic?' do
59
+ it 'returns true for climax beat type' do
60
+ climax = described_class.new(content: 'x', beat_type: :climax)
61
+ expect(climax.climactic?).to be true
62
+ end
63
+
64
+ it 'returns true when intensity is at or above climax threshold' do
65
+ intense = described_class.new(content: 'x', intensity: 0.9)
66
+ expect(intense.climactic?).to be true
67
+ end
68
+
69
+ it 'returns false for low-intensity non-climax beat' do
70
+ normal = described_class.new(content: 'x', beat_type: :rising_action, intensity: 0.3)
71
+ expect(normal.climactic?).to be false
72
+ end
73
+ end
74
+
75
+ describe '#resolving?' do
76
+ it 'returns true for falling_action' do
77
+ b = described_class.new(content: 'x', beat_type: :falling_action)
78
+ expect(b.resolving?).to be true
79
+ end
80
+
81
+ it 'returns true for resolution' do
82
+ b = described_class.new(content: 'x', beat_type: :resolution)
83
+ expect(b.resolving?).to be true
84
+ end
85
+
86
+ it 'returns true for denouement' do
87
+ b = described_class.new(content: 'x', beat_type: :denouement)
88
+ expect(b.resolving?).to be true
89
+ end
90
+
91
+ it 'returns false for rising_action' do
92
+ b = described_class.new(content: 'x', beat_type: :rising_action)
93
+ expect(b.resolving?).to be false
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Constants do
4
+ describe 'BEAT_TYPES' do
5
+ it 'contains all eight Freytag beat types' do
6
+ expect(described_class::BEAT_TYPES).to include(
7
+ :exposition, :rising_action, :complication, :crisis,
8
+ :climax, :falling_action, :resolution, :denouement
9
+ )
10
+ end
11
+
12
+ it 'is frozen' do
13
+ expect(described_class::BEAT_TYPES).to be_frozen
14
+ end
15
+ end
16
+
17
+ describe 'ARC_PHASES' do
18
+ it 'contains all four arc phases' do
19
+ expect(described_class::ARC_PHASES).to eq(%i[building peak resolving complete])
20
+ end
21
+ end
22
+
23
+ describe 'tension constants' do
24
+ it 'defines a default tension below climax threshold' do
25
+ expect(described_class::DEFAULT_TENSION).to be < described_class::CLIMAX_THRESHOLD
26
+ end
27
+
28
+ it 'defines resolution threshold below default tension' do
29
+ expect(described_class::RESOLUTION_THRESHOLD).to be < described_class::DEFAULT_TENSION
30
+ end
31
+
32
+ it 'defines TENSION_RISE greater than TENSION_FALL' do
33
+ expect(described_class::TENSION_RISE).to be > described_class::TENSION_FALL
34
+ end
35
+ end
36
+
37
+ describe '.label_for' do
38
+ it 'returns :calm for low tension values' do
39
+ expect(described_class.label_for(described_class::TENSION_LABELS, 0.1)).to eq(:calm)
40
+ end
41
+
42
+ it 'returns :critical for high tension values' do
43
+ expect(described_class.label_for(described_class::TENSION_LABELS, 0.9)).to eq(:critical)
44
+ end
45
+
46
+ it 'returns :developing for mid-range values' do
47
+ expect(described_class.label_for(described_class::TENSION_LABELS, 0.35)).to eq(:developing)
48
+ end
49
+
50
+ it 'returns :tense for values in the tense range' do
51
+ expect(described_class.label_for(described_class::TENSION_LABELS, 0.65)).to eq(:tense)
52
+ end
53
+
54
+ it 'returns :mundane for low drama score' do
55
+ expect(described_class.label_for(described_class::DRAMA_LABELS, 0.1)).to eq(:mundane)
56
+ end
57
+
58
+ it 'returns :gripping for high drama score' do
59
+ expect(described_class.label_for(described_class::DRAMA_LABELS, 0.9)).to eq(:gripping)
60
+ end
61
+
62
+ it 'falls back to last label when value is 1.0' do
63
+ result = described_class.label_for(described_class::TENSION_LABELS, 1.0)
64
+ expect(result).to eq(:critical)
65
+ end
66
+ end
67
+
68
+ describe 'PHASE_LABELS' do
69
+ it 'has a description for each arc phase' do
70
+ described_class::ARC_PHASES.each do |phase|
71
+ expect(described_class::PHASE_LABELS[phase]).to be_a(String)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/self/narrative_arc/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Self::NarrativeArc::Runners::Narrative do
6
+ let(:client) { Legion::Extensions::Agentic::Self::NarrativeArc::Client.new }
7
+ let(:engine) { Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::ArcEngine.new }
8
+
9
+ describe '#create_arc' do
10
+ it 'creates an arc and returns arc_id' do
11
+ result = client.create_arc(title: 'Debugging mystery')
12
+ expect(result[:success]).to be true
13
+ expect(result[:arc_id]).to match(/\A[0-9a-f-]{36}\z/)
14
+ expect(result[:arc_phase]).to eq(:building)
15
+ end
16
+
17
+ it 'stores arc in the engine' do
18
+ result = client.create_arc(title: 'Stored arc')
19
+ arcs = client.active_arcs
20
+ ids = arcs[:arcs].map { |a| a[:arc_id] }
21
+ expect(ids).to include(result[:arc_id])
22
+ end
23
+
24
+ it 'accepts custom initial tension' do
25
+ result = client.create_arc(title: 'Tense start', initial_tension: 0.6)
26
+ expect(result[:tension_level]).to eq(0.6)
27
+ end
28
+
29
+ it 'accepts an injected engine' do
30
+ result = client.create_arc(title: 'Injected', engine: engine)
31
+ expect(result[:success]).to be true
32
+ end
33
+
34
+ it 'returns failure when engine is at capacity' do
35
+ stub_const(
36
+ 'Legion::Extensions::Agentic::Self::NarrativeArc::Helpers::Constants::MAX_ARCS',
37
+ 0
38
+ )
39
+ result = client.create_arc(title: 'Overflow')
40
+ expect(result[:success]).to be false
41
+ expect(result[:reason]).to eq(:engine_at_capacity)
42
+ end
43
+ end
44
+
45
+ describe '#add_beat' do
46
+ let(:arc_id) { client.create_arc(title: 'Beat test arc')[:arc_id] }
47
+
48
+ it 'adds a beat and returns beat_id' do
49
+ result = client.add_beat(arc_id: arc_id, content: 'Something happened', beat_type: :rising_action)
50
+ expect(result[:success]).to be true
51
+ expect(result[:beat_id]).to match(/\A[0-9a-f-]{36}\z/)
52
+ end
53
+
54
+ it 'includes dramatic_score in result' do
55
+ result = client.add_beat(arc_id: arc_id, content: 'Escalation', beat_type: :complication, intensity: 0.7)
56
+ expect(result[:dramatic_score]).to be_a(Float)
57
+ end
58
+
59
+ it 'returns failure for unknown arc' do
60
+ result = client.add_beat(arc_id: 'bad-id', content: 'x')
61
+ expect(result[:success]).to be false
62
+ expect(result[:reason]).to eq(:arc_not_found)
63
+ end
64
+
65
+ it 'transitions arc phase as beats accumulate' do
66
+ client.add_beat(arc_id: arc_id, content: 'Climax!', beat_type: :climax, intensity: 0.95)
67
+ result = client.add_beat(arc_id: arc_id, content: 'Resolution', beat_type: :resolution)
68
+ expect(result[:arc_phase]).to eq(:complete)
69
+ end
70
+
71
+ it 'accepts an injected engine' do
72
+ injected_arc_id = client.create_arc(title: 'Injected arc', engine: engine)[:arc_id]
73
+ result = client.add_beat(arc_id: injected_arc_id, content: 'x', engine: engine)
74
+ expect(result[:success]).to be true
75
+ end
76
+ end
77
+
78
+ describe '#get_arc' do
79
+ it 'returns found arc with beats' do
80
+ arc_id = client.create_arc(title: 'Get test')[:arc_id]
81
+ client.add_beat(arc_id: arc_id, content: 'A beat')
82
+ result = client.get_arc(arc_id: arc_id)
83
+ expect(result[:found]).to be true
84
+ expect(result[:arc]).to include(:arc_id, :title)
85
+ expect(result[:beats].size).to eq(1)
86
+ end
87
+
88
+ it 'returns not found for missing arc' do
89
+ result = client.get_arc(arc_id: 'nonexistent')
90
+ expect(result[:found]).to be false
91
+ end
92
+ end
93
+
94
+ describe '#active_arcs' do
95
+ it 'returns all non-complete arcs' do
96
+ client.create_arc(title: 'Arc A')
97
+ client.create_arc(title: 'Arc B')
98
+ result = client.active_arcs
99
+ expect(result[:count]).to eq(2)
100
+ expect(result[:arcs].size).to eq(2)
101
+ end
102
+ end
103
+
104
+ describe '#completed_arcs' do
105
+ it 'returns arcs that have been resolved' do
106
+ arc_id = client.create_arc(title: 'Complete me')[:arc_id]
107
+ client.add_beat(arc_id: arc_id, content: 'Climax', beat_type: :climax, intensity: 0.95)
108
+ client.add_beat(arc_id: arc_id, content: 'Done', beat_type: :resolution)
109
+
110
+ result = client.completed_arcs
111
+ expect(result[:count]).to eq(1)
112
+ end
113
+ end
114
+
115
+ describe '#most_dramatic_arc' do
116
+ it 'returns not found when no arcs exist' do
117
+ result = client.most_dramatic_arc
118
+ expect(result[:found]).to be false
119
+ end
120
+
121
+ it 'returns the arc with the highest dramatic score' do
122
+ arc_a = client.create_arc(title: 'Boring')[:arc_id]
123
+ arc_b = client.create_arc(title: 'Gripping')[:arc_id]
124
+ client.add_beat(arc_id: arc_b, content: 'Explosion', beat_type: :climax, intensity: 0.99)
125
+ client.add_beat(arc_id: arc_b, content: 'Fallout', beat_type: :complication, intensity: 0.85)
126
+
127
+ result = client.most_dramatic_arc
128
+ expect(result[:found]).to be true
129
+ expect(result[:arc][:arc_id]).to eq(arc_b)
130
+ expect(result[:arc][:arc_id]).not_to eq(arc_a)
131
+ end
132
+ end
133
+
134
+ describe '#arc_report' do
135
+ it 'returns a complete report structure' do
136
+ client.create_arc(title: 'Report subject')
137
+ result = client.arc_report
138
+ expect(result[:success]).to be true
139
+ expect(result[:report]).to include(:total_arcs, :active, :completed, :patterns, :tension_dist)
140
+ end
141
+ end
142
+ end