anima-core 0.2.1 → 1.0.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,516 @@
1
+ # RSpec Matchers Reference
2
+
3
+ Comprehensive reference for RSpec's built-in matchers and custom matcher creation.
4
+
5
+ ## Equality Matchers
6
+
7
+ ### eq(expected)
8
+
9
+ Uses `==` operator. Diffable.
10
+
11
+ ```ruby
12
+ expect(5).to eq(5)
13
+ expect(actual).not_to eq(expected)
14
+ ```
15
+
16
+ ### eql(expected)
17
+
18
+ Uses `eql?` method (type-sensitive).
19
+
20
+ ```ruby
21
+ expect(5).to eql(5)
22
+ expect(5).not_to eql(5.0) # Integer vs Float
23
+ ```
24
+
25
+ ### equal(expected) / be(expected)
26
+
27
+ Uses `equal?` method (object identity).
28
+
29
+ ```ruby
30
+ expect(actual).to be(expected)
31
+ expect(actual).to equal(expected)
32
+ ```
33
+
34
+ ## Comparison Matchers
35
+
36
+ ### Operators
37
+
38
+ ```ruby
39
+ expect(5).to be > 3
40
+ expect(5).to be >= 5
41
+ expect(5).to be < 10
42
+ expect(5).to be <= 5
43
+ ```
44
+
45
+ ### be_within
46
+
47
+ ```ruby
48
+ expect(result).to be_within(0.5).of(3.0)
49
+ expect(Math::PI).to be_within(0.01).of(3.14)
50
+ ```
51
+
52
+ ### be_between
53
+
54
+ ```ruby
55
+ expect(5).to be_between(1, 10) # inclusive (default)
56
+ expect(5).to be_between(1, 10).inclusive
57
+ expect(5).to be_between(1, 5).exclusive # fails - 5 not in (1, 5)
58
+ ```
59
+
60
+ ## Type/Class Matchers
61
+
62
+ ### be_an_instance_of / be_instance_of
63
+
64
+ Uses `instance_of?` - exact class match.
65
+
66
+ ```ruby
67
+ expect(5).to be_an_instance_of(Integer)
68
+ expect(5).not_to be_an_instance_of(Numeric)
69
+ ```
70
+
71
+ ### be_a_kind_of / be_a / be_an
72
+
73
+ Uses `kind_of?` - includes ancestors.
74
+
75
+ ```ruby
76
+ expect(5).to be_a_kind_of(Integer)
77
+ expect(5).to be_a_kind_of(Numeric)
78
+ expect(5).to be_a(Integer)
79
+ expect(5).to be_an(Integer)
80
+ ```
81
+
82
+ ### respond_to
83
+
84
+ ```ruby
85
+ expect("string").to respond_to(:length)
86
+ expect(obj).to respond_to(:foo, :bar)
87
+ expect(obj).to respond_to(:foo).with(2).arguments
88
+ expect(obj).to respond_to(:bar).with_keywords(:a, :b)
89
+ ```
90
+
91
+ ## Truthiness Matchers
92
+
93
+ ### be_truthy
94
+
95
+ Passes for any value except `nil` and `false`.
96
+
97
+ ```ruby
98
+ expect(1).to be_truthy
99
+ expect("").to be_truthy
100
+ expect(nil).not_to be_truthy
101
+ ```
102
+
103
+ ### be_falsey / be_falsy
104
+
105
+ Passes for `nil` or `false`.
106
+
107
+ ```ruby
108
+ expect(nil).to be_falsey
109
+ expect(false).to be_falsy
110
+ expect(0).not_to be_falsey # 0 is truthy in Ruby
111
+ ```
112
+
113
+ ### be_nil
114
+
115
+ ```ruby
116
+ expect(nil).to be_nil
117
+ expect(false).not_to be_nil
118
+ ```
119
+
120
+ ### be true / be false
121
+
122
+ Exact boolean match.
123
+
124
+ ```ruby
125
+ expect(actual).to be true # actual == true
126
+ expect(actual).to be false # actual == false
127
+ ```
128
+
129
+ ### exist
130
+
131
+ Calls `exist?` or `exists?` method.
132
+
133
+ ```ruby
134
+ expect(File).to exist("path/to/file")
135
+ expect(obj).to exist
136
+ ```
137
+
138
+ ## Predicate Matchers
139
+
140
+ ### be_* (Dynamic)
141
+
142
+ Converts to predicate method call.
143
+
144
+ ```ruby
145
+ expect([]).to be_empty # [].empty?
146
+ expect(obj).to be_valid # obj.valid?
147
+ expect(user).to be_active # user.active?
148
+ expect(user).to be_an_admin # user.admin?
149
+ ```
150
+
151
+ ### have_* (Dynamic)
152
+
153
+ Converts to `has_*?` method call.
154
+
155
+ ```ruby
156
+ expect({a: 1}).to have_key(:a) # {a: 1}.has_key?(:a)
157
+ expect(list).to have_items # list.has_items?
158
+ ```
159
+
160
+ ## Collection Matchers
161
+
162
+ ### include
163
+
164
+ ```ruby
165
+ # Arrays
166
+ expect([1, 2, 3]).to include(1)
167
+ expect([1, 2, 3]).to include(1, 2)
168
+
169
+ # Strings
170
+ expect("hello").to include("ell")
171
+
172
+ # Hashes
173
+ expect({a: 1, b: 2}).to include(:a)
174
+ expect({a: 1, b: 2}).to include(a: 1)
175
+ expect({a: 1, b: 2}).to include(a: 1, b: 2)
176
+ ```
177
+
178
+ ### contain_exactly / match_array
179
+
180
+ Order-independent array matching.
181
+
182
+ ```ruby
183
+ expect([1, 2, 3]).to contain_exactly(3, 2, 1)
184
+ expect([1, 2, 3]).to match_array([3, 2, 1])
185
+ ```
186
+
187
+ ### start_with / end_with
188
+
189
+ ```ruby
190
+ expect("this string").to start_with("this")
191
+ expect([0, 1, 2]).to start_with(0, 1)
192
+
193
+ expect("this string").to end_with("ring")
194
+ expect([0, 1, 2, 3]).to end_with(2, 3)
195
+ ```
196
+
197
+ ### cover
198
+
199
+ For ranges.
200
+
201
+ ```ruby
202
+ expect(1..10).to cover(5)
203
+ expect(1..10).to cover(4, 6)
204
+ expect(1..10).not_to cover(11)
205
+ ```
206
+
207
+ ### all
208
+
209
+ Every element matches.
210
+
211
+ ```ruby
212
+ expect([1, 3, 5]).to all(be_odd)
213
+ expect([1, 3, 5]).to all(be_odd.and be_an(Integer))
214
+ ```
215
+
216
+ ### have_attributes
217
+
218
+ ```ruby
219
+ Person = Struct.new(:name, :age)
220
+ person = Person.new("Bob", 32)
221
+
222
+ expect(person).to have_attributes(name: "Bob", age: 32)
223
+ expect(person).to have_attributes(name: a_string_starting_with("B"))
224
+ ```
225
+
226
+ ## Pattern Matching
227
+
228
+ ### match
229
+
230
+ ```ruby
231
+ # Regex
232
+ expect(email).to match(/^[\w.]+@[\w.]+\.\w+$/)
233
+ expect("foo@example.com").to match("example.com")
234
+
235
+ # Nested data structures
236
+ expect(hash).to match(
237
+ a: {
238
+ b: a_collection_containing_exactly(
239
+ a_string_starting_with("f"),
240
+ an_instance_of(Integer)
241
+ )
242
+ }
243
+ )
244
+ ```
245
+
246
+ ## Change Observation
247
+
248
+ ### change
249
+
250
+ ```ruby
251
+ # Block form
252
+ expect { counter += 1 }.to change { counter }
253
+ expect { counter += 1 }.to change { counter }.by(1)
254
+ expect { counter += 1 }.to change { counter }.from(0).to(1)
255
+
256
+ # Object/method form
257
+ expect { user.save }.to change(user, :updated_at)
258
+ ```
259
+
260
+ ### Chaining
261
+
262
+ ```ruby
263
+ expect { x += 5 }.to change { x }.by(5)
264
+ expect { x += 5 }.to change { x }.by_at_least(3)
265
+ expect { x += 5 }.to change { x }.by_at_most(10)
266
+ expect { x = 10 }.to change { x }.from(0).to(10)
267
+ ```
268
+
269
+ ## Error Matchers
270
+
271
+ ### raise_error / raise_exception
272
+
273
+ ```ruby
274
+ # Any error
275
+ expect { raise }.to raise_error
276
+
277
+ # Specific class
278
+ expect { raise StandardError }.to raise_error(StandardError)
279
+
280
+ # With message
281
+ expect { raise "boom" }.to raise_error("boom")
282
+ expect { raise StandardError, "boom" }.to raise_error(StandardError, "boom")
283
+
284
+ # Message regex
285
+ expect { raise StandardError, "boom" }.to raise_error(StandardError, /boo/)
286
+
287
+ # Block for complex assertions
288
+ expect { raise StandardError, "boom" }.to raise_error { |error|
289
+ expect(error.message).to eq("boom")
290
+ }
291
+ ```
292
+
293
+ ### throw_symbol
294
+
295
+ ```ruby
296
+ expect { throw :done }.to throw_symbol
297
+ expect { throw :done }.to throw_symbol(:done)
298
+ expect { throw :done, "value" }.to throw_symbol(:done, "value")
299
+ ```
300
+
301
+ ## Output Matchers
302
+
303
+ ### output
304
+
305
+ ```ruby
306
+ # Stdout
307
+ expect { print "foo" }.to output.to_stdout
308
+ expect { print "foo" }.to output("foo").to_stdout
309
+ expect { print "foo" }.to output(/foo/).to_stdout
310
+
311
+ # Stderr
312
+ expect { warn "foo" }.to output.to_stderr
313
+ expect { warn "foo" }.to output("foo\n").to_stderr
314
+
315
+ # Subprocess
316
+ expect { system('echo foo') }.to output("foo\n").to_stdout_from_any_process
317
+ ```
318
+
319
+ ## Yield Matchers
320
+
321
+ All require block probe `|b|`.
322
+
323
+ ### yield_control
324
+
325
+ ```ruby
326
+ expect { |b| 5.tap(&b) }.to yield_control
327
+ expect { |b| "a".to_sym }.not_to yield_control
328
+ ```
329
+
330
+ ### yield_with_no_args
331
+
332
+ ```ruby
333
+ expect { |b| User.transaction(&b) }.to yield_with_no_args
334
+ ```
335
+
336
+ ### yield_with_args
337
+
338
+ ```ruby
339
+ expect { |b| 5.tap(&b) }.to yield_with_args
340
+ expect { |b| 5.tap(&b) }.to yield_with_args(5)
341
+ expect { |b| 5.tap(&b) }.to yield_with_args(Integer)
342
+ ```
343
+
344
+ ### yield_successive_args
345
+
346
+ ```ruby
347
+ expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
348
+ expect { |b| {a: 1, b: 2}.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
349
+ ```
350
+
351
+ ## Satisfy Matcher
352
+
353
+ Custom predicate logic - useful for complex conditions.
354
+
355
+ ```ruby
356
+ expect(5).to satisfy { |n| n > 3 }
357
+ expect(5).to satisfy("be greater than 3") { |n| n > 3 }
358
+
359
+ # Complex validation
360
+ expect(response).to satisfy("be valid JSON with user data") { |r|
361
+ json = JSON.parse(r.body)
362
+ json["user"].present? && json["user"]["id"].is_a?(Integer)
363
+ }
364
+ ```
365
+
366
+ ## Composing Matchers
367
+
368
+ ### .and / &
369
+
370
+ ```ruby
371
+ expect(alphabet).to start_with("a").and end_with("z")
372
+ expect(alphabet).to start_with("a") & end_with("z")
373
+ expect([1, 3, 5]).to all(be_odd.and be_an(Integer))
374
+ ```
375
+
376
+ ### .or / |
377
+
378
+ ```ruby
379
+ expect(color).to eq("red").or eq("green").or eq("yellow")
380
+ expect(color).to eq("red") | eq("green") | eq("yellow")
381
+ ```
382
+
383
+ ### Matcher Arguments
384
+
385
+ Matchers can accept other matchers as arguments:
386
+
387
+ ```ruby
388
+ # Change with matcher
389
+ expect { k += 1.05 }.to change { k }.by(a_value_within(0.1).of(1.0))
390
+
391
+ # Collection with matchers
392
+ expect([1, 2.5]).to contain_exactly(
393
+ an_instance_of(Integer),
394
+ a_value_within(0.1).of(2.5)
395
+ )
396
+
397
+ # Include with nested matcher
398
+ expect(hash).to include(a: a_string_matching(/foo/))
399
+ ```
400
+
401
+ ## Composable Aliases
402
+
403
+ Every matcher has noun-form aliases for composition:
404
+
405
+ | Verb Form | Noun Form |
406
+ |-----------|-----------|
407
+ | `eq(x)` | `an_object_eq_to(x)` |
408
+ | `be_within(x).of(y)` | `a_value_within(x).of(y)` |
409
+ | `start_with(x)` | `a_string_starting_with(x)` |
410
+ | `include(x)` | `a_collection_including(x)` |
411
+ | `be_a(X)` | `a_kind_of(X)` |
412
+ | `be_an_instance_of(X)` | `an_instance_of(X)` |
413
+
414
+ ## Aggregate Failures
415
+
416
+ Collect multiple failures instead of stopping at first:
417
+
418
+ ```ruby
419
+ aggregate_failures("user validation") do
420
+ expect(user.name).to eq("Bob")
421
+ expect(user.age).to be > 18
422
+ expect(user.email).to match(/@/)
423
+ end
424
+ ```
425
+
426
+ ## Custom Matchers
427
+
428
+ ### Using DSL
429
+
430
+ ```ruby
431
+ RSpec::Matchers.define :be_in_zone do |zone|
432
+ match do |player|
433
+ player.in_zone?(zone)
434
+ end
435
+
436
+ failure_message do |player|
437
+ "expected #{player} to be in zone #{zone}"
438
+ end
439
+
440
+ failure_message_when_negated do |player|
441
+ "expected #{player} not to be in zone #{zone}"
442
+ end
443
+
444
+ description do
445
+ "be in zone #{zone}"
446
+ end
447
+ end
448
+
449
+ # With chaining
450
+ RSpec::Matchers.define :have_errors_on do |key|
451
+ chain :with do |message|
452
+ @message = message
453
+ end
454
+
455
+ match do |actual|
456
+ actual.errors[key] == @message
457
+ end
458
+ end
459
+
460
+ expect(user).to have_errors_on(:email).with("can't be blank")
461
+ ```
462
+
463
+ ### Available DSL Methods
464
+
465
+ - `match(options = {}, &block)` - Main matching logic
466
+ - `match_when_negated(&block)` - Custom negative matching
467
+ - `failure_message(&block)` - Positive failure message
468
+ - `failure_message_when_negated(&block)` - Negative failure message
469
+ - `description(&block)` - Matcher description
470
+ - `diffable` - Enable diff output
471
+ - `supports_block_expectations` - Allow `expect { }`
472
+ - `chain(method, *attrs, &block)` - Fluent interface methods
473
+
474
+ ### From Scratch
475
+
476
+ ```ruby
477
+ class BeInZone
478
+ include RSpec::Matchers::Composable
479
+
480
+ def initialize(expected)
481
+ @expected = expected
482
+ end
483
+
484
+ def matches?(actual)
485
+ @actual = actual
486
+ @actual.in_zone?(@expected)
487
+ end
488
+
489
+ def failure_message
490
+ "expected #{@actual.inspect} to be in zone #{@expected}"
491
+ end
492
+
493
+ def failure_message_when_negated
494
+ "expected #{@actual.inspect} not to be in zone #{@expected}"
495
+ end
496
+
497
+ def description
498
+ "be in zone #{@expected}"
499
+ end
500
+ end
501
+
502
+ def be_in_zone(expected)
503
+ BeInZone.new(expected)
504
+ end
505
+ ```
506
+
507
+ ### Aliasing and Negation
508
+
509
+ ```ruby
510
+ # Create alias with description transformation
511
+ RSpec::Matchers.alias_matcher :a_list_that_sums_to, :sum_to
512
+
513
+ # Create negated matcher
514
+ RSpec::Matchers.define_negated_matcher :exclude, :include
515
+ expect([1, 2]).to exclude(3)
516
+ ```