anima-core 0.3.0 → 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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  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 +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +168 -0
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -1
@@ -0,0 +1,738 @@
1
+ # ActiveRecord Callbacks Reference
2
+
3
+ Comprehensive reference for model callbacks: lifecycle hooks, transaction callbacks, ordering, halting behavior, and when to use callbacks vs alternatives.
4
+
5
+ ## Callback Execution Order
6
+
7
+ ### Creating a Record
8
+
9
+ ```
10
+ 1. before_validation
11
+ 2. after_validation
12
+ 3. before_save
13
+ 4. around_save (before yield)
14
+ 5. before_create
15
+ 6. around_create (before yield)
16
+ ─── INSERT ───
17
+ 7. around_create (after yield)
18
+ 8. after_create
19
+ 9. around_save (after yield)
20
+ 10. after_save
21
+ ─── COMMIT ───
22
+ 11. after_commit / after_rollback
23
+ ```
24
+
25
+ ### Updating a Record
26
+
27
+ Same as create, but `before_update`, `around_update`, `after_update` replace create callbacks.
28
+
29
+ ### Destroying a Record
30
+
31
+ ```
32
+ 1. before_destroy
33
+ 2. around_destroy (before yield)
34
+ ─── DELETE ───
35
+ 3. around_destroy (after yield)
36
+ 4. after_destroy
37
+ ─── COMMIT ───
38
+ 5. after_commit / after_rollback
39
+ ```
40
+
41
+ ### Special Callbacks
42
+
43
+ ```ruby
44
+ after_initialize # After new() or loading from DB
45
+ after_find # After loading from DB (before after_initialize)
46
+ after_touch # After touch() is called
47
+ ```
48
+
49
+ ## Callback Declaration
50
+
51
+ ### Method Reference (Preferred)
52
+
53
+ ```ruby
54
+ class Article < ApplicationRecord
55
+ before_save :normalize_title
56
+ after_create :notify_subscribers
57
+
58
+ private
59
+
60
+ def normalize_title
61
+ self.title = title.strip.titleize
62
+ end
63
+
64
+ def notify_subscribers
65
+ NotificationJob.perform_later(id)
66
+ end
67
+ end
68
+ ```
69
+
70
+ ### Inline Block
71
+
72
+ ```ruby
73
+ class User < ApplicationRecord
74
+ before_validation { self.email = email&.downcase&.strip }
75
+
76
+ after_create do |user|
77
+ AuditLog.create!(action: "user_created", record: user)
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### Callback Object
83
+
84
+ ```ruby
85
+ class AuditLogger
86
+ def after_create(record)
87
+ AuditLog.create!(action: "created", record:)
88
+ end
89
+
90
+ def after_update(record)
91
+ AuditLog.create!(action: "updated", record:, changes: record.previous_changes)
92
+ end
93
+ end
94
+
95
+ class Order < ApplicationRecord
96
+ after_create AuditLogger.new
97
+ after_update AuditLogger.new
98
+ end
99
+ ```
100
+
101
+ ## Conditional Callbacks
102
+
103
+ ```ruby
104
+ # Symbol (method name) - preferred for readability
105
+ before_save :normalize_card_number, if: :paid_with_card?
106
+
107
+ # Proc/Lambda - for one-liners
108
+ after_create :send_welcome_email, if: -> { email.present? }
109
+
110
+ # Multiple conditions (all must be true)
111
+ before_validation :set_defaults, if: [:new_record?, :draft?]
112
+
113
+ # Using both :if and :unless
114
+ after_save :sync_to_search, if: :published?, unless: :skip_indexing?
115
+ ```
116
+
117
+ ## Halting the Callback Chain
118
+
119
+ Use `throw :abort` in `before_*` callbacks to halt execution:
120
+
121
+ ```ruby
122
+ class Order < ApplicationRecord
123
+ before_save :check_stock
124
+
125
+ private
126
+
127
+ def check_stock
128
+ throw(:abort) if items.any? { |item| item.out_of_stock? }
129
+ end
130
+ end
131
+ ```
132
+
133
+ **Behavior when halted:**
134
+ - `save` returns `false`
135
+ - `save!` raises `ActiveRecord::RecordNotSaved`
136
+ - `destroy` returns `false`
137
+ - `destroy!` raises `ActiveRecord::RecordNotDestroyed`
138
+ - Transaction is rolled back
139
+
140
+ **Important**: `throw :abort` does NOT add errors. Add them explicitly:
141
+
142
+ ```ruby
143
+ def check_stock
144
+ if items.any?(&:out_of_stock?)
145
+ errors.add(:base, "Some items are out of stock")
146
+ throw(:abort)
147
+ end
148
+ end
149
+ ```
150
+
151
+ ## Transaction Callbacks
152
+
153
+ ### The Critical Distinction
154
+
155
+ All `after_*` callbacks run INSIDE the transaction. External systems can't see your changes yet:
156
+
157
+ ```ruby
158
+ # WRONG - Race condition!
159
+ after_save :enqueue_processing
160
+
161
+ def enqueue_processing
162
+ ProcessingJob.perform_later(id) # Job starts before COMMIT
163
+ # Sidekiq: "Couldn't find Record with 'id'=123"
164
+ end
165
+ ```
166
+
167
+ Use `after_commit` for external interactions:
168
+
169
+ ```ruby
170
+ # CORRECT - Runs after COMMIT
171
+ after_commit :enqueue_processing, on: :create
172
+
173
+ def enqueue_processing
174
+ ProcessingJob.perform_later(id) # Record guaranteed to exist
175
+ end
176
+ ```
177
+
178
+ ### When to Use after_commit
179
+
180
+ - Enqueuing background jobs
181
+ - Updating search indexes (Elasticsearch, Algolia)
182
+ - Clearing caches
183
+ - Sending emails/notifications
184
+ - Making API calls to external services
185
+ - Any action that should only occur if the DB change is permanent
186
+
187
+ ### Transaction Callback Variants
188
+
189
+ ```ruby
190
+ # Fires on create, update, or destroy after commit
191
+ after_commit :refresh_cache
192
+
193
+ # Scoped to specific actions (Rails 7.1+)
194
+ after_create_commit :send_welcome_email
195
+ after_update_commit :sync_changes
196
+ after_destroy_commit :cleanup_external
197
+
198
+ # Equivalent to the above
199
+ after_commit :send_welcome_email, on: :create
200
+ after_commit :sync_changes, on: :update
201
+ after_commit :cleanup_external, on: :destroy
202
+
203
+ # Multiple actions
204
+ after_commit :reindex, on: [:create, :update]
205
+
206
+ # Rollback callback
207
+ after_rollback :log_failure
208
+ ```
209
+
210
+ ### after_save_commit (Rails 7.1+)
211
+
212
+ ```ruby
213
+ # Fires on create OR update, not destroy
214
+ after_save_commit :sync_to_search
215
+ ```
216
+
217
+ ## Transaction Callback Gotchas
218
+
219
+ ### Gotcha 1: Callback Deduplication
220
+
221
+ ```ruby
222
+ # WRONG - Only the last one runs!
223
+ after_commit :do_something
224
+ after_commit :do_something
225
+
226
+ # Also deduplicated across variants
227
+ after_commit :sync_data
228
+ after_create_commit :sync_data
229
+ after_save_commit :sync_data
230
+ # Only one sync_data callback runs
231
+
232
+ # CORRECT - Use :on option
233
+ after_commit :sync_data, on: [:create, :update]
234
+ ```
235
+
236
+ ### Gotcha 2: previous_changes Behavior
237
+
238
+ `previous_changes` is reset on each save, not when the transaction closes:
239
+
240
+ ```ruby
241
+ after_commit :log_changes
242
+
243
+ def log_changes
244
+ # If record was saved twice in one transaction,
245
+ # previous_changes only contains the LAST save's changes
246
+ end
247
+ ```
248
+
249
+ ### Gotcha 3: Exception Handling
250
+
251
+ Exceptions in `after_commit` callbacks:
252
+ - Bubble up to the caller
253
+ - Stop remaining `after_commit` callbacks from running
254
+ - Do NOT rollback (commit already happened)
255
+
256
+ ```ruby
257
+ after_commit :might_fail
258
+ after_commit :wont_run_if_above_fails
259
+
260
+ def might_fail
261
+ ExternalService.notify(self) # Raises exception
262
+ rescue ExternalService::Error => e
263
+ Rails.logger.error("Notification failed: #{e}")
264
+ # Don't re-raise - let other callbacks run
265
+ end
266
+ ```
267
+
268
+ ### Gotcha 4: Testing Complications
269
+
270
+ Older Rails wrapped tests in transactions, preventing `after_commit` from firing. Fixed in Rails 5+, but be aware:
271
+
272
+ ```ruby
273
+ # Use transactional fixtures carefully
274
+ # after_commit runs in Rails 5+ with proper config
275
+ ```
276
+
277
+ ### Gotcha 5: Callback Ordering (Rails 7.1+)
278
+
279
+ ```ruby
280
+ # Rails 7.1+ default: callbacks run in definition order
281
+ config.active_record.run_after_transaction_callbacks_in_order_defined = true
282
+
283
+ # Pre-7.1 behavior: reverse order
284
+ config.active_record.run_after_transaction_callbacks_in_order_defined = false
285
+ ```
286
+
287
+ ## Around Callbacks
288
+
289
+ Must call `yield` or the action won't execute:
290
+
291
+ ```ruby
292
+ class Article < ApplicationRecord
293
+ around_save :measure_save_time
294
+
295
+ private
296
+
297
+ def measure_save_time
298
+ start = Time.current
299
+ yield # REQUIRED - executes the save
300
+ duration = Time.current - start
301
+ Rails.logger.info("Save took #{duration}s")
302
+ end
303
+ end
304
+ ```
305
+
306
+ **Forgetting yield is a common bug** - the record won't be saved.
307
+
308
+ ## Callback Ordering with prepend
309
+
310
+ Callbacks from associations (like `dependent: :destroy`) run before your callbacks. Use `prepend: true` to run first:
311
+
312
+ ```ruby
313
+ class Topic < ApplicationRecord
314
+ has_many :comments, dependent: :destroy
315
+
316
+ # WRONG - comments already deleted when this runs
317
+ before_destroy :log_comments
318
+
319
+ # CORRECT - runs before dependent: :destroy
320
+ before_destroy :log_comments, prepend: true
321
+
322
+ private
323
+
324
+ def log_comments
325
+ Rails.logger.info("Destroying topic with #{comments.count} comments")
326
+ end
327
+ end
328
+ ```
329
+
330
+ ## Callback Inheritance
331
+
332
+ Callbacks are inherited by subclasses:
333
+
334
+ ```ruby
335
+ class Animal < ApplicationRecord
336
+ before_save :set_kingdom
337
+ end
338
+
339
+ class Dog < Animal
340
+ before_save :set_species
341
+ end
342
+
343
+ # Dog.create runs both: set_kingdom, then set_species
344
+ ```
345
+
346
+ **Critical**: Define callbacks BEFORE associations in parent classes for proper inheritance.
347
+
348
+ ## Methods That Skip Callbacks
349
+
350
+ These methods bypass ALL callbacks:
351
+
352
+ | Method | Skips Callbacks |
353
+ |--------|-----------------|
354
+ | `delete` | Yes |
355
+ | `delete_all` | Yes |
356
+ | `update_column` | Yes |
357
+ | `update_columns` | Yes |
358
+ | `update_all` | Yes |
359
+ | `insert` / `insert_all` | Yes |
360
+ | `upsert` / `upsert_all` | Yes |
361
+ | `touch_all` | Yes |
362
+ | `increment!` / `decrement!` | Yes |
363
+ | `increment_counter` / `decrement_counter` | Yes |
364
+
365
+ **Warning**: Use with caution - you may bypass critical business logic.
366
+
367
+ ## Debugging Callbacks
368
+
369
+ Inspect the callback chain:
370
+
371
+ ```ruby
372
+ # All save callbacks
373
+ Article._save_callbacks
374
+
375
+ # Only before_save callbacks
376
+ Article._save_callbacks.select { |cb| cb.kind == :before }
377
+
378
+ # Check if a specific callback is registered
379
+ Article._save_callbacks.map(&:filter).include?(:normalize_title)
380
+
381
+ # All validation callbacks
382
+ Article._validation_callbacks
383
+
384
+ # All create callbacks
385
+ Article._create_callbacks
386
+ ```
387
+
388
+ ## Anti-Patterns
389
+
390
+ ### 1. Callback Hell
391
+
392
+ ```ruby
393
+ # WRONG - Too much responsibility, hard to test
394
+ class User < ApplicationRecord
395
+ after_create :send_welcome_email
396
+ after_create :create_default_settings
397
+ after_create :notify_admin
398
+ after_create :sync_to_crm
399
+ after_create :update_analytics
400
+ after_update :sync_to_crm
401
+ after_update :invalidate_cache
402
+ after_destroy :cleanup_external_data
403
+ # ... 20 more callbacks
404
+ end
405
+
406
+ # BETTER - Use a service object
407
+ class UserCreationService
408
+ def call(user_params)
409
+ user = User.create!(user_params)
410
+ send_welcome_email(user)
411
+ create_default_settings(user)
412
+ notify_admin(user)
413
+ sync_to_crm(user)
414
+ user
415
+ end
416
+ end
417
+ ```
418
+
419
+ ### 2. Callbacks Modifying Other Models
420
+
421
+ ```ruby
422
+ # WRONG - Violates Law of Demeter
423
+ class Message < ApplicationRecord
424
+ after_create :update_conversation_stats
425
+
426
+ def update_conversation_stats
427
+ conversation.update!(
428
+ message_count: conversation.messages.count,
429
+ last_message_at: created_at
430
+ )
431
+ end
432
+ end
433
+
434
+ # BETTER - Use a service or let the parent handle it
435
+ class ConversationMessageService
436
+ def add_message(conversation, message_params)
437
+ message = conversation.messages.create!(message_params)
438
+ conversation.touch(:last_message_at)
439
+ conversation.increment!(:message_count)
440
+ message
441
+ end
442
+ end
443
+ ```
444
+
445
+ ### 3. Using after_save for External Systems
446
+
447
+ ```ruby
448
+ # WRONG - Race condition with background jobs
449
+ after_save :enqueue_processing
450
+
451
+ # CORRECT
452
+ after_commit :enqueue_processing, on: [:create, :update]
453
+ ```
454
+
455
+ ### 4. Heavy Operations in Callbacks
456
+
457
+ ```ruby
458
+ # WRONG - Blocks the request
459
+ after_create :generate_thumbnail
460
+ after_create :sync_to_external_api
461
+ after_create :send_notification
462
+
463
+ # CORRECT - Defer to background jobs
464
+ after_create_commit :enqueue_post_creation_jobs
465
+
466
+ def enqueue_post_creation_jobs
467
+ ThumbnailJob.perform_later(id)
468
+ ExternalSyncJob.perform_later(id)
469
+ NotificationJob.perform_later(id)
470
+ end
471
+ ```
472
+
473
+ ### 5. Conditional Logic Explosion
474
+
475
+ ```ruby
476
+ # WRONG - Hard to follow
477
+ before_save :do_a, if: :condition_x?
478
+ before_save :do_b, if: :condition_y?
479
+ before_save :do_c, if: -> { condition_x? && !condition_z? }
480
+ after_save :do_d, unless: -> { condition_x? || condition_y? }
481
+
482
+ # BETTER - Extract to a single callback or service
483
+ before_save :prepare_for_save
484
+
485
+ def prepare_for_save
486
+ if condition_x?
487
+ do_a
488
+ do_c unless condition_z?
489
+ end
490
+ do_b if condition_y?
491
+ end
492
+ ```
493
+
494
+ ### 6. Throwing abort Without Error Messages
495
+
496
+ ```ruby
497
+ # WRONG - No feedback to user
498
+ before_save :validate_complex_rules
499
+
500
+ def validate_complex_rules
501
+ throw(:abort) if invalid_state?
502
+ end
503
+
504
+ # CORRECT - Add error message
505
+ def validate_complex_rules
506
+ if invalid_state?
507
+ errors.add(:base, "Cannot save in current state")
508
+ throw(:abort)
509
+ end
510
+ end
511
+ ```
512
+
513
+ ## When Callbacks Are Appropriate
514
+
515
+ **Good uses:**
516
+ - Setting defaults or computed attributes on the current model
517
+ - Data normalization (strip, downcase, format)
518
+ - Simple audit logging (who changed what)
519
+ - Counter cache updates
520
+ - Maintaining data consistency within the same model
521
+
522
+ ```ruby
523
+ class User < ApplicationRecord
524
+ before_validation :normalize_email
525
+ before_create :generate_api_key
526
+ after_touch :update_full_name_cache
527
+
528
+ private
529
+
530
+ def normalize_email
531
+ self.email = email&.downcase&.strip
532
+ end
533
+
534
+ def generate_api_key
535
+ self.api_key ||= SecureRandom.hex(32)
536
+ end
537
+
538
+ def update_full_name_cache
539
+ update_column(:full_name, "#{first_name} #{last_name}")
540
+ end
541
+ end
542
+ ```
543
+
544
+ ## Alternatives to Callbacks
545
+
546
+ ### Service Objects (Recommended)
547
+
548
+ ```ruby
549
+ # app/services/user_registration_service.rb
550
+ class UserRegistrationService
551
+ def call(params)
552
+ user = User.create!(params)
553
+ WelcomeMailer.welcome(user).deliver_later
554
+ Analytics.track("user_registered", user_id: user.id)
555
+ CrmSync.create_contact(user)
556
+ user
557
+ end
558
+ end
559
+
560
+ # In controller
561
+ def create
562
+ user = UserRegistrationService.new.call(user_params)
563
+ redirect_to user
564
+ end
565
+ ```
566
+
567
+ **Benefits:**
568
+ - Explicit control flow
569
+ - Easy to test in isolation
570
+ - Clear dependencies
571
+ - No hidden side effects
572
+
573
+ ### Domain Events
574
+
575
+ ```ruby
576
+ # Using a simple pub/sub pattern
577
+ class User < ApplicationRecord
578
+ after_create_commit { EventBus.publish("user.created", self) }
579
+ end
580
+
581
+ # Subscribers
582
+ EventBus.subscribe("user.created") do |user|
583
+ WelcomeMailer.welcome(user).deliver_later
584
+ end
585
+
586
+ EventBus.subscribe("user.created") do |user|
587
+ Analytics.track("user_registered", user_id: user.id)
588
+ end
589
+ ```
590
+
591
+ **Benefits:**
592
+ - Loose coupling
593
+ - Easy to add/remove handlers
594
+ - Better for complex event-driven architectures
595
+
596
+ ### Form Objects
597
+
598
+ ```ruby
599
+ # app/forms/registration_form.rb
600
+ class RegistrationForm
601
+ include ActiveModel::Model
602
+
603
+ attr_accessor :email, :password, :terms_accepted
604
+
605
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
606
+ validates :password, length: { minimum: 8 }
607
+ validates :terms_accepted, acceptance: true
608
+
609
+ def save
610
+ return false unless valid?
611
+
612
+ user = User.create!(email:, password:)
613
+ send_welcome_email(user)
614
+ true
615
+ end
616
+
617
+ private
618
+
619
+ def send_welcome_email(user)
620
+ WelcomeMailer.welcome(user).deliver_later
621
+ end
622
+ end
623
+ ```
624
+
625
+ ## Testing Callbacks
626
+
627
+ ### Test the Behavior, Not the Callback
628
+
629
+ ```ruby
630
+ # WRONG - Testing implementation
631
+ it "calls normalize_email before validation" do
632
+ expect(user).to receive(:normalize_email)
633
+ user.valid?
634
+ end
635
+
636
+ # CORRECT - Testing behavior
637
+ it "normalizes email before saving" do
638
+ user = User.create!(email: " JOHN@EXAMPLE.COM ", ...)
639
+ expect(user.email).to eq("john@example.com")
640
+ end
641
+ ```
642
+
643
+ ### Testing after_commit Callbacks
644
+
645
+ ```ruby
646
+ # Ensure test database connection commits
647
+ # In rails_helper.rb or spec_helper.rb
648
+
649
+ RSpec.configure do |config|
650
+ config.use_transactional_fixtures = true
651
+ # Rails 5+ properly handles after_commit in tests
652
+ end
653
+
654
+ # Test the side effect
655
+ it "enqueues a job after creation" do
656
+ expect {
657
+ User.create!(email: "test@example.com")
658
+ }.to have_enqueued_job(WelcomeEmailJob)
659
+ end
660
+ ```
661
+
662
+ ### Isolating Callback Effects
663
+
664
+ ```ruby
665
+ # Skip callbacks when not relevant to test
666
+ RSpec.describe User do
667
+ describe "#full_name" do
668
+ it "returns first and last name" do
669
+ user = User.new(first_name: "John", last_name: "Doe")
670
+ expect(user.full_name).to eq("John Doe")
671
+ end
672
+ end
673
+ end
674
+
675
+ # Don't need to trigger callbacks for this test
676
+ ```
677
+
678
+ ## Performance Considerations
679
+
680
+ ### Callbacks Slow Down Tests
681
+
682
+ Heavy use of callbacks in factories creates cascading effects:
683
+
684
+ ```ruby
685
+ # Slow - every User.create runs all callbacks
686
+ FactoryBot.define do
687
+ factory :user do
688
+ email { Faker::Internet.email }
689
+ end
690
+ end
691
+
692
+ # Faster - use build_stubbed when callbacks aren't needed
693
+ let(:user) { build_stubbed(:user) }
694
+ ```
695
+
696
+ ### Consider Database Triggers
697
+
698
+ For high-performance counter caches:
699
+
700
+ ```ruby
701
+ # ActiveRecord callback - runs in Ruby, per record
702
+ after_create { parent.increment!(:children_count) }
703
+
704
+ # Database trigger - runs in DB, faster for bulk operations
705
+ # See strong_migrations gem for safe trigger management
706
+ ```
707
+
708
+ ## Nested Transactions
709
+
710
+ Callbacks run inside the transaction. Nested transactions without `requires_new: true` don't create savepoints:
711
+
712
+ ```ruby
713
+ User.transaction do
714
+ user = User.create!(name: "Alice")
715
+
716
+ User.transaction do
717
+ user.update!(name: "Bob")
718
+ raise ActiveRecord::Rollback # Does NOT rollback!
719
+ end
720
+ end
721
+ # User saved as "Bob" - the rollback was ignored
722
+ ```
723
+
724
+ Use `requires_new: true` for independent rollback:
725
+
726
+ ```ruby
727
+ User.transaction do
728
+ user = User.create!(name: "Alice")
729
+
730
+ User.transaction(requires_new: true) do
731
+ user.update!(name: "Bob")
732
+ raise ActiveRecord::Rollback # Creates savepoint, rolls back to "Alice"
733
+ end
734
+ end
735
+ # User saved as "Alice"
736
+ ```
737
+
738
+ **PostgreSQL Warning**: Don't rescue `ActiveRecord::StatementInvalid` inside transactions - it poisons the transaction.