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,367 @@
1
+ # ActiveRecord Validation Contexts Examples
2
+
3
+ # =============================================================================
4
+ # BUILT-IN CONTEXTS: :create AND :update
5
+ # =============================================================================
6
+
7
+ class User < ApplicationRecord
8
+ # Always validates (no :on option)
9
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
10
+
11
+ # Only on create (new records)
12
+ validates :password, presence: true, length: { minimum: 8 }, on: :create
13
+ validates :password_confirmation, presence: true, on: :create
14
+ validates :terms_accepted, acceptance: true, on: :create
15
+
16
+ # Only on update (existing records)
17
+ validates :reason_for_change, presence: true, on: :update
18
+ end
19
+
20
+ # Usage:
21
+ # user = User.new(email: "test@example.com")
22
+ # user.valid? # Checks email (always) + password, password_confirmation, terms (create)
23
+ # user.valid?(:create) # Same as above
24
+ # user.valid?(:update) # Checks email (always) + reason_for_change (update)
25
+
26
+ # =============================================================================
27
+ # CUSTOM CONTEXTS
28
+ # =============================================================================
29
+
30
+ class Article < ApplicationRecord
31
+ # Basic validations (always run)
32
+ validates :title, presence: true
33
+ validates :body, presence: true
34
+
35
+ # Publishing context - stricter requirements
36
+ validates :meta_description, presence: true, on: :publish
37
+ validates :meta_keywords, presence: true, on: :publish
38
+ validates :featured_image, presence: true, on: :publish
39
+ validates :category_id, presence: true, on: :publish
40
+ validates :published_at, presence: true, on: :publish
41
+
42
+ # Archiving context
43
+ validates :archived_reason, presence: true, on: :archive
44
+ validates :archived_by_id, presence: true, on: :archive
45
+
46
+ # Feature context
47
+ validates :featured_position, presence: true,
48
+ numericality: { greater_than: 0 }, on: :feature
49
+
50
+ def publish!
51
+ self.published_at ||= Time.current
52
+ self.status = "published"
53
+ save!(context: :publish)
54
+ end
55
+
56
+ def archive!(reason:, by:)
57
+ self.archived_reason = reason
58
+ self.archived_by_id = by.id
59
+ self.status = "archived"
60
+ save!(context: :archive)
61
+ end
62
+
63
+ def feature!(position:)
64
+ self.featured_position = position
65
+ self.status = "featured"
66
+ save!(context: :feature)
67
+ end
68
+ end
69
+
70
+ # Usage:
71
+ # article = Article.new(title: "Hello", body: "World")
72
+ # article.valid? # true - basic validations pass
73
+ # article.valid?(:publish) # false - missing meta_description, etc.
74
+ # article.publish! # Raises if publish validations fail
75
+
76
+ # =============================================================================
77
+ # MULTIPLE CONTEXTS
78
+ # =============================================================================
79
+
80
+ class Document < ApplicationRecord
81
+ validates :title, presence: true
82
+ validates :content, presence: true
83
+
84
+ # Runs on both create and custom context
85
+ validates :author_id, presence: true, on: [:create, :submit]
86
+
87
+ # Runs on multiple custom contexts
88
+ validates :reviewer_id, presence: true, on: [:review, :approve, :reject]
89
+ validates :review_notes, presence: true, on: [:approve, :reject]
90
+
91
+ # Different validations for different approval levels
92
+ validates :manager_approval, inclusion: { in: [true] }, on: :final_approve
93
+ validates :director_approval, inclusion: { in: [true] }, on: :final_approve
94
+ end
95
+
96
+ # =============================================================================
97
+ # CONTEXT BEHAVIOR MATRIX
98
+ # =============================================================================
99
+
100
+ class ContextDemo < ApplicationRecord
101
+ validates :always_field, presence: true # No :on
102
+ validates :create_field, presence: true, on: :create
103
+ validates :update_field, presence: true, on: :update
104
+ validates :custom_field, presence: true, on: :custom
105
+ validates :multi_field, presence: true, on: [:create, :custom]
106
+ end
107
+
108
+ # Behavior:
109
+ # .valid? => always_field, create_field (if new), update_field (if persisted)
110
+ # .valid?(:create) => always_field, create_field
111
+ # .valid?(:update) => always_field, update_field
112
+ # .valid?(:custom) => always_field, custom_field
113
+ # save => uses :create for new, :update for existing
114
+ # save(context: :x) => uses custom context :x
115
+
116
+ # =============================================================================
117
+ # WORKFLOW-BASED CONTEXTS (STATE MACHINE STYLE)
118
+ # =============================================================================
119
+
120
+ class Order < ApplicationRecord
121
+ # Base validations
122
+ validates :customer_id, presence: true
123
+ validates :line_items, presence: true
124
+
125
+ # Context: placing order
126
+ validates :billing_address, presence: true, on: :place
127
+ validates :shipping_address, presence: true, on: :place
128
+ validates :payment_method, presence: true, on: :place
129
+
130
+ # Context: processing payment
131
+ validates :payment_token, presence: true, on: :process_payment
132
+ validates :payment_amount, numericality: { greater_than: 0 }, on: :process_payment
133
+
134
+ # Context: shipping
135
+ validates :tracking_number, presence: true, on: :ship
136
+ validates :carrier, presence: true, on: :ship
137
+ validates :shipped_at, presence: true, on: :ship
138
+
139
+ # Context: completing
140
+ validates :delivered_at, presence: true, on: :complete
141
+
142
+ # Context: canceling
143
+ validates :cancellation_reason, presence: true, on: :cancel
144
+ validates :cancelled_at, presence: true, on: :cancel
145
+
146
+ def place!
147
+ save!(context: :place)
148
+ end
149
+
150
+ def process_payment!(token:)
151
+ self.payment_token = token
152
+ self.payment_amount = total
153
+ save!(context: :process_payment)
154
+ end
155
+
156
+ def ship!(tracking:, carrier:)
157
+ self.tracking_number = tracking
158
+ self.carrier = carrier
159
+ self.shipped_at = Time.current
160
+ save!(context: :ship)
161
+ end
162
+
163
+ def complete!
164
+ self.delivered_at = Time.current
165
+ save!(context: :complete)
166
+ end
167
+
168
+ def cancel!(reason:)
169
+ self.cancellation_reason = reason
170
+ self.cancelled_at = Time.current
171
+ save!(context: :cancel)
172
+ end
173
+ end
174
+
175
+ # =============================================================================
176
+ # MULTI-STEP FORM CONTEXTS
177
+ # =============================================================================
178
+
179
+ class Registration < ApplicationRecord
180
+ # Step 1: Basic info
181
+ validates :email, presence: true, on: :step_one
182
+ validates :password, presence: true, length: { minimum: 8 }, on: :step_one
183
+
184
+ # Step 2: Personal info
185
+ validates :first_name, presence: true, on: :step_two
186
+ validates :last_name, presence: true, on: :step_two
187
+ validates :date_of_birth, presence: true, on: :step_two
188
+
189
+ # Step 3: Preferences
190
+ validates :newsletter_preference, inclusion: { in: [true, false] }, on: :step_three
191
+ validates :terms_accepted, acceptance: true, on: :step_three
192
+
193
+ # Final validation includes all steps
194
+ validates :email, :password, presence: true, on: :finalize
195
+ validates :first_name, :last_name, :date_of_birth, presence: true, on: :finalize
196
+ validates :terms_accepted, acceptance: true, on: :finalize
197
+
198
+ def complete_step!(step)
199
+ context = "step_#{step}".to_sym
200
+ save!(context:)
201
+ end
202
+
203
+ def finalize!
204
+ save!(context: :finalize)
205
+ end
206
+ end
207
+
208
+ # Controller usage:
209
+ # def step_one
210
+ # @registration.assign_attributes(step_one_params)
211
+ # if @registration.valid?(:step_one)
212
+ # @registration.save(validate: false)
213
+ # redirect_to step_two_path
214
+ # else
215
+ # render :step_one
216
+ # end
217
+ # end
218
+
219
+ # =============================================================================
220
+ # PERMISSION-BASED CONTEXTS
221
+ # =============================================================================
222
+
223
+ class Post < ApplicationRecord
224
+ validates :title, presence: true
225
+ validates :body, presence: true
226
+
227
+ # Regular user context
228
+ validates :content_warning, presence: true, on: :user_publish,
229
+ if: :contains_sensitive_content?
230
+
231
+ # Admin context - can skip content warning
232
+ # No additional validations needed
233
+
234
+ # Moderator context
235
+ validates :moderation_note, presence: true, on: :moderator_edit
236
+
237
+ def publish_as_user!
238
+ save!(context: :user_publish)
239
+ end
240
+
241
+ def publish_as_admin!
242
+ save! # No special context, uses defaults
243
+ end
244
+
245
+ def edit_as_moderator!
246
+ save!(context: :moderator_edit)
247
+ end
248
+
249
+ private
250
+
251
+ def contains_sensitive_content?
252
+ # Check for sensitive keywords
253
+ sensitive_keywords = %w[violence explicit]
254
+ sensitive_keywords.any? { |kw| body&.downcase&.include?(kw) }
255
+ end
256
+ end
257
+
258
+ # =============================================================================
259
+ # CHECKING CONTEXT IN CONDITIONALS
260
+ # =============================================================================
261
+
262
+ class Invoice < ApplicationRecord
263
+ validates :amount, presence: true
264
+ validates :due_date, presence: true
265
+
266
+ # Access current context in conditions
267
+ validates :tax_number, presence: true,
268
+ if: -> { validation_context == :business }
269
+
270
+ validates :personal_id, presence: true,
271
+ if: -> { validation_context == :personal }
272
+
273
+ # Combine context check with other conditions
274
+ validates :rush_fee, presence: true,
275
+ if: -> { validation_context == :rush && amount > 1000 }
276
+ end
277
+
278
+ # =============================================================================
279
+ # CONTEXT WITH validates_with
280
+ # =============================================================================
281
+
282
+ class ComplexOrderValidator < ActiveModel::Validator
283
+ def validate(record)
284
+ case record.validation_context
285
+ when :place
286
+ validate_for_placement(record)
287
+ when :ship
288
+ validate_for_shipping(record)
289
+ else
290
+ validate_basic(record)
291
+ end
292
+ end
293
+
294
+ private
295
+
296
+ def validate_basic(record)
297
+ record.errors.add(:base, "Order must have items") if record.line_items.empty?
298
+ end
299
+
300
+ def validate_for_placement(record)
301
+ validate_basic(record)
302
+ validate_inventory(record)
303
+ validate_payment_method(record)
304
+ end
305
+
306
+ def validate_for_shipping(record)
307
+ validate_address(record)
308
+ validate_weight(record)
309
+ end
310
+
311
+ def validate_inventory(record)
312
+ record.line_items.each do |item|
313
+ unless item.product.in_stock?(item.quantity)
314
+ record.errors.add(:base, "#{item.product.name} is out of stock")
315
+ end
316
+ end
317
+ end
318
+
319
+ def validate_payment_method(record)
320
+ unless record.payment_method&.valid_for?(record.total)
321
+ record.errors.add(:payment_method, "is not valid for this order")
322
+ end
323
+ end
324
+
325
+ def validate_address(record)
326
+ unless record.shipping_address&.deliverable?
327
+ record.errors.add(:shipping_address, "is not a valid shipping destination")
328
+ end
329
+ end
330
+
331
+ def validate_weight(record)
332
+ if record.total_weight > 50.kilograms
333
+ record.errors.add(:base, "Order exceeds maximum shipping weight")
334
+ end
335
+ end
336
+ end
337
+
338
+ class Order < ApplicationRecord
339
+ validates_with ComplexOrderValidator
340
+ end
341
+
342
+ # =============================================================================
343
+ # ANTI-PATTERNS TO AVOID
344
+ # =============================================================================
345
+
346
+ # ANTI-PATTERN: Mixing contexts with conditionals incorrectly
347
+ class BadExample < ApplicationRecord
348
+ # This won't work as expected!
349
+ # When you call valid?(:admin), the :create context is NOT also applied
350
+ validates :field, presence: true, on: :create
351
+ validates :admin_field, presence: true, on: :admin
352
+
353
+ # If you call valid?(:admin) on a new record,
354
+ # the :create validation is NOT run!
355
+ end
356
+
357
+ # BETTER: Explicitly include all needed contexts
358
+ class GoodExample < ApplicationRecord
359
+ validates :field, presence: true # No :on, runs always
360
+ validates :admin_field, presence: true, on: :admin
361
+ end
362
+
363
+ # Or use multiple contexts
364
+ class AlsoGood < ApplicationRecord
365
+ validates :field, presence: true, on: [:create, :admin]
366
+ validates :admin_field, presence: true, on: :admin
367
+ end