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,447 @@
1
+ # Alternatives to Callbacks
2
+ # Demonstrates service objects, form objects, and domain events as callback alternatives
3
+
4
+ # =============================================================================
5
+ # Problem: Callback Hell
6
+ # =============================================================================
7
+
8
+ # ANTI-PATTERN: Too many responsibilities in callbacks
9
+ class UserWithCallbackHell < ApplicationRecord
10
+ after_create :send_welcome_email
11
+ after_create :create_default_settings
12
+ after_create :notify_admin
13
+ after_create :sync_to_crm
14
+ after_create :create_audit_log
15
+ after_create :track_signup_analytics
16
+ after_create :create_free_trial
17
+ after_create :send_to_marketing_list
18
+ after_update :sync_to_crm
19
+ after_update :update_search_index
20
+ after_update :notify_changes
21
+ after_destroy :cleanup_associated_data
22
+ after_destroy :notify_admin
23
+
24
+ # Problems:
25
+ # - Hard to test each piece in isolation
26
+ # - Hard to skip individual side effects
27
+ # - Unclear execution order
28
+ # - Slow tests (all callbacks fire on User.create!)
29
+ # - Tight coupling
30
+ end
31
+
32
+ # =============================================================================
33
+ # Solution 1: Service Objects (Recommended)
34
+ # =============================================================================
35
+
36
+ # Clean model with minimal callbacks
37
+ class User < ApplicationRecord
38
+ # Only keep callbacks for data integrity on the model itself
39
+ before_validation :normalize_email
40
+ before_create :generate_api_key
41
+
42
+ private
43
+
44
+ def normalize_email
45
+ self.email = email&.downcase&.strip
46
+ end
47
+
48
+ def generate_api_key
49
+ self.api_key ||= SecureRandom.hex(32)
50
+ end
51
+ end
52
+
53
+ # Service object encapsulates the full registration workflow
54
+ class UserRegistrationService
55
+ def initialize(user_params, referral_code: nil)
56
+ @user_params = user_params
57
+ @referral_code = referral_code
58
+ end
59
+
60
+ def call
61
+ user = User.new(@user_params)
62
+
63
+ ActiveRecord::Base.transaction do
64
+ user.save!
65
+ create_default_settings(user)
66
+ apply_referral(user) if @referral_code
67
+ end
68
+
69
+ # Post-transaction side effects
70
+ send_welcome_email(user)
71
+ sync_to_crm(user)
72
+ track_analytics(user)
73
+ notify_admin(user)
74
+
75
+ user
76
+ rescue ActiveRecord::RecordInvalid => e
77
+ e.record
78
+ end
79
+
80
+ private
81
+
82
+ def create_default_settings(user)
83
+ UserSettings.create!(
84
+ user:,
85
+ theme: "light",
86
+ notifications_enabled: true,
87
+ timezone: "UTC"
88
+ )
89
+ end
90
+
91
+ def apply_referral(user)
92
+ referrer = User.find_by(referral_code: @referral_code)
93
+ return unless referrer
94
+
95
+ Referral.create!(referrer:, referred: user)
96
+ ReferralRewardJob.perform_later(referrer.id)
97
+ end
98
+
99
+ def send_welcome_email(user)
100
+ WelcomeMailer.welcome(user).deliver_later
101
+ end
102
+
103
+ def sync_to_crm(user)
104
+ CrmSyncJob.perform_later(user.id)
105
+ end
106
+
107
+ def track_analytics(user)
108
+ Analytics.track("user_signed_up", user_id: user.id)
109
+ end
110
+
111
+ def notify_admin(user)
112
+ AdminNotificationJob.perform_later("new_user", user.id)
113
+ end
114
+ end
115
+
116
+ # Controller usage
117
+ class UsersController < ApplicationController
118
+ def create
119
+ result = UserRegistrationService.new(user_params, referral_code: params[:ref]).call
120
+
121
+ if result.persisted?
122
+ redirect_to dashboard_path, notice: "Welcome!"
123
+ else
124
+ @user = result
125
+ render :new, status: :unprocessable_entity
126
+ end
127
+ end
128
+ end
129
+
130
+ # =============================================================================
131
+ # Solution 2: Form Objects
132
+ # =============================================================================
133
+
134
+ # For complex forms that span multiple models or need special validation
135
+ class RegistrationForm
136
+ include ActiveModel::Model
137
+ include ActiveModel::Attributes
138
+
139
+ attribute :email, :string
140
+ attribute :password, :string
141
+ attribute :password_confirmation, :string
142
+ attribute :terms_accepted, :boolean
143
+ attribute :company_name, :string
144
+ attribute :plan, :string, default: "free"
145
+
146
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
147
+ validates :password, presence: true, length: { minimum: 8 }, confirmation: true
148
+ validates :terms_accepted, acceptance: true
149
+ validates :company_name, presence: true
150
+ validates :plan, inclusion: { in: %w[free starter business] }
151
+
152
+ def save
153
+ return false unless valid?
154
+
155
+ ActiveRecord::Base.transaction do
156
+ create_user
157
+ create_company
158
+ create_subscription
159
+ end
160
+
161
+ send_notifications
162
+ true
163
+ rescue ActiveRecord::RecordInvalid => e
164
+ errors.add(:base, e.message)
165
+ false
166
+ end
167
+
168
+ def user
169
+ @user
170
+ end
171
+
172
+ private
173
+
174
+ def create_user
175
+ @user = User.create!(email:, password:)
176
+ end
177
+
178
+ def create_company
179
+ @company = Company.create!(name: company_name, owner: @user)
180
+ @user.update!(company: @company)
181
+ end
182
+
183
+ def create_subscription
184
+ Subscription.create!(company: @company, plan:, status: "active")
185
+ end
186
+
187
+ def send_notifications
188
+ WelcomeMailer.welcome(@user).deliver_later
189
+ CompanyMailer.setup_guide(@company).deliver_later
190
+ end
191
+ end
192
+
193
+ # Controller usage
194
+ class RegistrationsController < ApplicationController
195
+ def create
196
+ @form = RegistrationForm.new(registration_params)
197
+
198
+ if @form.save
199
+ sign_in(@form.user)
200
+ redirect_to onboarding_path
201
+ else
202
+ render :new, status: :unprocessable_entity
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def registration_params
209
+ params.require(:registration).permit(
210
+ :email, :password, :password_confirmation,
211
+ :terms_accepted, :company_name, :plan
212
+ )
213
+ end
214
+ end
215
+
216
+ # =============================================================================
217
+ # Solution 3: Domain Events
218
+ # =============================================================================
219
+
220
+ # Simple event bus implementation
221
+ class EventBus
222
+ class << self
223
+ def subscribers
224
+ @subscribers ||= Hash.new { |h, k| h[k] = [] }
225
+ end
226
+
227
+ def subscribe(event_name, &handler)
228
+ subscribers[event_name] << handler
229
+ end
230
+
231
+ def publish(event_name, payload)
232
+ subscribers[event_name].each do |handler|
233
+ handler.call(payload)
234
+ rescue => e
235
+ Rails.logger.error("Event handler failed: #{e.message}")
236
+ ErrorTracker.capture(e)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ # Model publishes events
243
+ class Order < ApplicationRecord
244
+ after_commit :publish_created, on: :create
245
+ after_commit :publish_status_changed, on: :update, if: :saved_change_to_status?
246
+
247
+ private
248
+
249
+ def publish_created
250
+ EventBus.publish("order.created", order: self)
251
+ end
252
+
253
+ def publish_status_changed
254
+ EventBus.publish("order.status_changed", order: self, old_status: status_before_last_save, new_status: status)
255
+ end
256
+ end
257
+
258
+ # Event subscribers (in config/initializers/event_subscriptions.rb)
259
+ EventBus.subscribe("order.created") do |payload|
260
+ order = payload[:order]
261
+ OrderMailer.confirmation(order).deliver_later
262
+ end
263
+
264
+ EventBus.subscribe("order.created") do |payload|
265
+ order = payload[:order]
266
+ InventoryService.reserve_items(order)
267
+ end
268
+
269
+ EventBus.subscribe("order.created") do |payload|
270
+ order = payload[:order]
271
+ Analytics.track("order_placed", order_id: order.id, amount: order.total)
272
+ end
273
+
274
+ EventBus.subscribe("order.status_changed") do |payload|
275
+ order = payload[:order]
276
+ next unless payload[:new_status] == "shipped"
277
+
278
+ ShippingMailer.shipped(order).deliver_later
279
+ end
280
+
281
+ EventBus.subscribe("order.status_changed") do |payload|
282
+ order = payload[:order]
283
+ next unless payload[:new_status] == "delivered"
284
+
285
+ ReviewRequestMailer.request_review(order).deliver_later
286
+ end
287
+
288
+ # =============================================================================
289
+ # Solution 4: Interactor Pattern
290
+ # =============================================================================
291
+
292
+ # Using the interactor gem pattern
293
+ class CreateOrder
294
+ include Interactor::Organizer
295
+
296
+ organize ValidateCart, CalculateTotals, ProcessPayment, CreateOrderRecord, SendConfirmation
297
+
298
+ def call
299
+ context.cart = context.user.cart
300
+ end
301
+ end
302
+
303
+ class ValidateCart
304
+ include Interactor
305
+
306
+ def call
307
+ if context.cart.empty?
308
+ context.fail!(error: "Cart is empty")
309
+ end
310
+
311
+ if context.cart.items.any?(&:out_of_stock?)
312
+ context.fail!(error: "Some items are out of stock")
313
+ end
314
+ end
315
+ end
316
+
317
+ class CalculateTotals
318
+ include Interactor
319
+
320
+ def call
321
+ context.subtotal = context.cart.items.sum(&:total)
322
+ context.tax = context.subtotal * 0.1
323
+ context.total = context.subtotal + context.tax
324
+ end
325
+ end
326
+
327
+ class ProcessPayment
328
+ include Interactor
329
+
330
+ def call
331
+ result = PaymentGateway.charge(context.payment_method, context.total)
332
+
333
+ if result.success?
334
+ context.payment_id = result.payment_id
335
+ else
336
+ context.fail!(error: result.error_message)
337
+ end
338
+ end
339
+
340
+ def rollback
341
+ PaymentGateway.refund(context.payment_id) if context.payment_id
342
+ end
343
+ end
344
+
345
+ class CreateOrderRecord
346
+ include Interactor
347
+
348
+ def call
349
+ context.order = Order.create!(
350
+ user: context.user,
351
+ subtotal: context.subtotal,
352
+ tax: context.tax,
353
+ total: context.total,
354
+ payment_id: context.payment_id
355
+ )
356
+
357
+ context.cart.items.each do |cart_item|
358
+ context.order.line_items.create!(
359
+ product: cart_item.product,
360
+ quantity: cart_item.quantity,
361
+ price: cart_item.price
362
+ )
363
+ end
364
+ end
365
+
366
+ def rollback
367
+ context.order&.destroy
368
+ end
369
+ end
370
+
371
+ class SendConfirmation
372
+ include Interactor
373
+
374
+ def call
375
+ OrderMailer.confirmation(context.order).deliver_later
376
+ Analytics.track("order_completed", order_id: context.order.id)
377
+ end
378
+ end
379
+
380
+ # Controller usage
381
+ class OrdersController < ApplicationController
382
+ def create
383
+ result = CreateOrder.call(user: current_user, payment_method: params[:payment_method])
384
+
385
+ if result.success?
386
+ redirect_to order_path(result.order)
387
+ else
388
+ flash[:error] = result.error
389
+ redirect_to cart_path
390
+ end
391
+ end
392
+ end
393
+
394
+ # =============================================================================
395
+ # When Callbacks ARE Appropriate
396
+ # =============================================================================
397
+
398
+ # Good: Simple data normalization
399
+ class Email < ApplicationRecord
400
+ before_validation :normalize_address
401
+
402
+ private
403
+
404
+ def normalize_address
405
+ self.address = address&.downcase&.strip
406
+ end
407
+ end
408
+
409
+ # Good: Setting computed attributes
410
+ class LineItem < ApplicationRecord
411
+ before_save :calculate_total
412
+
413
+ private
414
+
415
+ def calculate_total
416
+ self.total = quantity * unit_price
417
+ end
418
+ end
419
+
420
+ # Good: Generating defaults
421
+ class ApiKey < ApplicationRecord
422
+ before_create :generate_key
423
+
424
+ private
425
+
426
+ def generate_key
427
+ self.key = SecureRandom.hex(32)
428
+ self.expires_at = 1.year.from_now
429
+ end
430
+ end
431
+
432
+ # Good: Simple counter maintenance (though DB triggers might be better)
433
+ class Comment < ApplicationRecord
434
+ belongs_to :post, counter_cache: true
435
+ end
436
+
437
+ # Good: Maintaining timestamps
438
+ class Document < ApplicationRecord
439
+ before_update :track_modification
440
+
441
+ private
442
+
443
+ def track_modification
444
+ self.last_modified_by = Current.user
445
+ self.revision_count = (revision_count || 0) + 1
446
+ end
447
+ end