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,596 @@
1
+ # ActiveRecord Validations Reference
2
+
3
+ Comprehensive reference for model validations: built-in validators, conditional validations, custom validators, validation contexts, and the critical distinction between model validations and database constraints.
4
+
5
+ ## Built-in Validators
6
+
7
+ ### Presence
8
+
9
+ ```ruby
10
+ validates :name, presence: true
11
+ validates :title, :body, presence: true # Multiple attributes
12
+ ```
13
+
14
+ **Boolean Fields Gotcha**: `false.blank? == true`, so presence validation fails on `false`:
15
+
16
+ ```ruby
17
+ # WRONG - fails when field is false
18
+ validates :active, presence: true
19
+
20
+ # CORRECT - use inclusion for booleans
21
+ validates :active, inclusion: { in: [true, false] }
22
+ ```
23
+
24
+ ### Uniqueness
25
+
26
+ ```ruby
27
+ validates :email, uniqueness: true
28
+ validates :username, uniqueness: { case_sensitive: false }
29
+ validates :code, uniqueness: { scope: :account_id } # Per-account uniqueness
30
+ validates :slug, uniqueness: { scope: [:category_id, :year] } # Composite scope
31
+ ```
32
+
33
+ **Critical**: Always pair with database unique index. See [Uniqueness Race Conditions](#uniqueness-race-conditions).
34
+
35
+ ### Format
36
+
37
+ ```ruby
38
+ validates :legacy_code, format: { with: /\A[A-Z]{3}-\d{4}\z/ }
39
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
40
+ ```
41
+
42
+ **Security Warning**: Use `\A` and `\z` for string boundaries, NOT `^` and `$`:
43
+
44
+ ```ruby
45
+ # WRONG - ^ and $ match line boundaries, vulnerable to injection
46
+ validates :code, format: { with: /^[a-z]+$/ }
47
+
48
+ # CORRECT - \A and \z match string boundaries
49
+ validates :code, format: { with: /\A[a-z]+\z/ }
50
+
51
+ # If multiline is intentional, be explicit
52
+ validates :bio, format: { with: /^[a-z]+$/, multiline: true }
53
+ ```
54
+
55
+ ### Length
56
+
57
+ ```ruby
58
+ validates :password, length: { minimum: 8 }
59
+ validates :bio, length: { maximum: 500 }
60
+ validates :pin, length: { is: 6 }
61
+ validates :username, length: { in: 3..20 } # Range
62
+ validates :content, length: { minimum: 10, too_short: "must have at least %{count} characters" }
63
+ ```
64
+
65
+ **Note**: `:maximum` alone allows nil by default (unlike `:minimum` or `:is`).
66
+
67
+ ### Numericality
68
+
69
+ ```ruby
70
+ validates :age, numericality: true
71
+ validates :quantity, numericality: { only_integer: true }
72
+ validates :price, numericality: { greater_than: 0 }
73
+ validates :age, numericality: { greater_than_or_equal_to: 18, less_than: 150 }
74
+ validates :discount, numericality: { in: 0..100 }
75
+ ```
76
+
77
+ | Option | Description |
78
+ |--------|-------------|
79
+ | `only_integer` | Must be integer (uses regex) |
80
+ | `only_numeric` | Must be Numeric instance (no string parsing) |
81
+ | `greater_than` | > value |
82
+ | `greater_than_or_equal_to` | >= value |
83
+ | `less_than` | < value |
84
+ | `less_than_or_equal_to` | <= value |
85
+ | `equal_to` | == value |
86
+ | `in` | Within range |
87
+ | `other_than` | != value |
88
+ | `odd` / `even` | Must be odd/even |
89
+
90
+ ### Inclusion / Exclusion
91
+
92
+ ```ruby
93
+ validates :status, inclusion: { in: %w[draft published archived] }
94
+ validates :role, inclusion: { in: %w[admin user guest], message: "%{value} is not valid" }
95
+ validates :legacy_code, exclusion: { in: %w[RESERVED SYSTEM] }
96
+ ```
97
+
98
+ **Performance**: Rails uses `cover?` for numeric/time ranges (fast), `include?` for others.
99
+
100
+ ### Confirmation
101
+
102
+ ```ruby
103
+ validates :password, confirmation: true
104
+ validates :password_confirmation, presence: true, if: :password_changed? # Required!
105
+ ```
106
+
107
+ **Note**: Confirmation only validates if `_confirmation` field is non-nil. Add explicit presence check.
108
+
109
+ ### Acceptance
110
+
111
+ ```ruby
112
+ validates :terms_of_service, acceptance: true
113
+ validates :eula, acceptance: { accept: ["yes", "1", true] }
114
+ ```
115
+
116
+ Default accepts `"1"` (from HTML checkbox) and `true`.
117
+
118
+ ### Associated
119
+
120
+ ```ruby
121
+ validates :author, presence: true # Ensure association exists
122
+ validates_associated :chapters # Validate associated records too
123
+ ```
124
+
125
+ **Warning**: Never use `validates_associated` on both ends of an association - causes infinite recursion.
126
+
127
+ ### Comparison
128
+
129
+ ```ruby
130
+ validates :end_date, comparison: { greater_than: :start_date }
131
+ validates :password, comparison: { other_than: :username }
132
+ ```
133
+
134
+ ## Conditional Validations
135
+
136
+ ### Using :if and :unless
137
+
138
+ ```ruby
139
+ # Symbol (method name) - preferred for readability
140
+ validates :card_number, presence: true, if: :paid_with_card?
141
+
142
+ # Proc/Lambda - for one-liners
143
+ validates :password, confirmation: true, unless: -> { password.blank? }
144
+
145
+ # Multiple conditions (all must be true)
146
+ validates :discount, presence: true,
147
+ if: [:premium_user?, :promotional_period?]
148
+
149
+ # Array with mixed types
150
+ validates :special_field, presence: true,
151
+ if: [:admin?, -> { feature_enabled?(:beta) }]
152
+ ```
153
+
154
+ ### Grouping with with_options
155
+
156
+ ```ruby
157
+ with_options if: :is_admin? do |admin|
158
+ admin.validates :password, length: { minimum: 10 }
159
+ admin.validates :email, format: { with: /@company\.com\z/ }
160
+ end
161
+ ```
162
+
163
+ ### Dynamic allow_nil / allow_blank
164
+
165
+ ```ruby
166
+ validates :nickname, length: { minimum: 3 },
167
+ allow_blank: -> { signup_step < 3 }
168
+ ```
169
+
170
+ ## Custom Validators
171
+
172
+ ### Inline Validation Method
173
+
174
+ ```ruby
175
+ class Invoice < ApplicationRecord
176
+ validate :total_matches_line_items
177
+
178
+ private
179
+
180
+ def total_matches_line_items
181
+ calculated = line_items.sum(&:amount)
182
+ return if total == calculated
183
+
184
+ errors.add(:total, "doesn't match line items (expected #{calculated})")
185
+ end
186
+ end
187
+ ```
188
+
189
+ ### EachValidator Class (Reusable)
190
+
191
+ ```ruby
192
+ # app/validators/email_validator.rb
193
+ class EmailValidator < ActiveModel::EachValidator
194
+ def validate_each(record, attribute, value)
195
+ return if value.blank? && options[:allow_blank]
196
+
197
+ unless URI::MailTo::EMAIL_REGEXP.match?(value)
198
+ record.errors.add(attribute, options[:message] || "is not a valid email")
199
+ end
200
+ end
201
+ end
202
+
203
+ # Usage in model
204
+ class User < ApplicationRecord
205
+ validates :email, email: true
206
+ validates :backup_email, email: { allow_blank: true }
207
+ end
208
+ ```
209
+
210
+ ### Full Validator Class (Record-Level)
211
+
212
+ ```ruby
213
+ # app/validators/date_range_validator.rb
214
+ class DateRangeValidator < ActiveModel::Validator
215
+ def validate(record)
216
+ return unless record.start_date && record.end_date
217
+
218
+ if record.end_date <= record.start_date
219
+ record.errors.add(:end_date, "must be after start date")
220
+ end
221
+ end
222
+ end
223
+
224
+ # Usage
225
+ class Event < ApplicationRecord
226
+ validates_with DateRangeValidator
227
+ end
228
+ ```
229
+
230
+ ## Validation Contexts
231
+
232
+ ### Built-in Contexts
233
+
234
+ ```ruby
235
+ validates :email, uniqueness: true, on: :create # Only on new records
236
+ validates :reason, presence: true, on: :update # Only on updates
237
+ validates :name, presence: true # Always (no :on option)
238
+ ```
239
+
240
+ ### Custom Contexts
241
+
242
+ ```ruby
243
+ class Article < ApplicationRecord
244
+ validates :title, presence: true
245
+ validates :body, presence: true
246
+ validates :published_at, presence: true, on: :publish
247
+ validates :reviewer_id, presence: true, on: :publish
248
+
249
+ def publish!
250
+ self.published_at = Time.current
251
+ save!(context: :publish)
252
+ end
253
+ end
254
+
255
+ # Usage
256
+ article.valid? # Checks title, body only
257
+ article.valid?(:publish) # Checks title, body, published_at, reviewer_id
258
+ article.publish! # Runs :publish context validations
259
+ ```
260
+
261
+ ### Multiple Contexts
262
+
263
+ ```ruby
264
+ validates :secret_key, presence: true, on: [:create, :regenerate]
265
+ ```
266
+
267
+ ### Context Behavior Matrix
268
+
269
+ | Validation | `.valid?` | `.valid?(:create)` | `.valid?(:update)` | `.valid?(:custom)` |
270
+ |------------|-----------|--------------------|--------------------|---------------------|
271
+ | No `on:` | Runs | Runs | Runs | Runs |
272
+ | `on: :create` | Runs | Runs | Skips | Skips |
273
+ | `on: :update` | Runs | Skips | Runs | Skips |
274
+ | `on: :custom` | Runs | Skips | Skips | Runs |
275
+
276
+ **Note**: Validations without `on:` run in ALL contexts.
277
+
278
+ ## Validation vs Database Constraint
279
+
280
+ ### Decision Framework
281
+
282
+ Ask two questions:
283
+
284
+ 1. **"Am I preventing bad data from being written?"** → Use database constraint
285
+ 2. **"Am I preventing user-fixable errors?"** → Use model validation
286
+
287
+ **Best practice**: Use both for critical fields.
288
+
289
+ ### Comparison
290
+
291
+ | Aspect | Model Validation | Database Constraint |
292
+ |--------|------------------|---------------------|
293
+ | User-friendly errors | Yes | Cryptic errors |
294
+ | Race condition safe | No | Yes |
295
+ | Can be bypassed | Yes (`update_all`, `insert_all`) | No |
296
+ | Database agnostic | Yes | May vary |
297
+ | Easy to test | Yes | Harder |
298
+ | Data integrity guarantee | Weak | Strong |
299
+
300
+ ### Implementation Pattern
301
+
302
+ ```ruby
303
+ # Migration - database constraints (data integrity)
304
+ class CreateUsers < ActiveRecord::Migration[7.2]
305
+ def change
306
+ create_table :users do |t|
307
+ t.string :email, null: false
308
+ t.string :username, null: false
309
+ t.integer :age
310
+ t.timestamps
311
+ end
312
+
313
+ add_index :users, :email, unique: true
314
+ add_index :users, :username, unique: true
315
+ add_check_constraint :users, "age >= 18", name: "users_age_check"
316
+ end
317
+ end
318
+
319
+ # Model - validations (user experience)
320
+ class User < ApplicationRecord
321
+ validates :email, presence: true, uniqueness: true,
322
+ format: { with: URI::MailTo::EMAIL_REGEXP }
323
+ validates :username, presence: true, uniqueness: { case_sensitive: false },
324
+ length: { in: 3..20 }
325
+ validates :age, numericality: { greater_than_or_equal_to: 18 }, allow_nil: true
326
+ end
327
+ ```
328
+
329
+ ### When to Use Database Constraints
330
+
331
+ - **Always** for uniqueness (race conditions)
332
+ - **Always** for NOT NULL on critical fields
333
+ - **Always** for foreign keys (referential integrity)
334
+ - When data can be written outside Rails (other apps, raw SQL, imports)
335
+ - When validation bypass methods are used (`update_all`, `insert_all`, etc.)
336
+
337
+ ### When Model Validation Is Enough
338
+
339
+ - Format validations (regex patterns)
340
+ - Complex business logic validations
341
+ - Validations that depend on other objects
342
+ - Validations needing user-friendly error messages
343
+
344
+ ## Uniqueness Race Conditions
345
+
346
+ ### The Problem
347
+
348
+ ```
349
+ Request 1: Check DB → "alice@example.com" doesn't exist → Continue
350
+ Request 2: Check DB → "alice@example.com" doesn't exist → Continue
351
+ Request 1: INSERT INTO users (email) VALUES ('alice@example.com') ✓
352
+ Request 2: INSERT INTO users (email) VALUES ('alice@example.com') ✓ DUPLICATE!
353
+ ```
354
+
355
+ ### The Solution
356
+
357
+ ```ruby
358
+ # Migration - REQUIRED
359
+ add_index :users, :email, unique: true
360
+
361
+ # Model - for user-friendly errors
362
+ validates :email, uniqueness: true
363
+ ```
364
+
365
+ ### Handling the Exception
366
+
367
+ ```ruby
368
+ def create
369
+ @user = User.new(user_params)
370
+ @user.save!
371
+ rescue ActiveRecord::RecordNotUnique
372
+ @user.errors.add(:email, "has already been taken")
373
+ render :new, status: :unprocessable_entity
374
+ end
375
+ ```
376
+
377
+ ### Alternative: find_or_create_by
378
+
379
+ ```ruby
380
+ # Idempotent - won't create duplicates
381
+ user = User.find_or_create_by(email: params[:email]) do |u|
382
+ u.name = params[:name]
383
+ end
384
+
385
+ # With race condition handling
386
+ user = User.create_or_find_by(email: params[:email]) do |u|
387
+ u.name = params[:name]
388
+ end
389
+ ```
390
+
391
+ ## Strict Validations
392
+
393
+ Raise exception instead of adding error:
394
+
395
+ ```ruby
396
+ validates :api_key, presence: true, strict: true
397
+ # Raises ActiveModel::StrictValidationFailed
398
+
399
+ validates :token, length: { is: 32 }, strict: TokenLengthException
400
+ # Raises TokenLengthException
401
+
402
+ validates! :internal_id, presence: true # Same as strict: true
403
+ ```
404
+
405
+ Use for programmer errors (not user input errors).
406
+
407
+ ## Anti-Patterns
408
+
409
+ ### 1. Uniqueness Without Database Index
410
+
411
+ ```ruby
412
+ # WRONG - race conditions possible
413
+ validates :email, uniqueness: true
414
+
415
+ # CORRECT - add unique index in migration
416
+ add_index :users, :email, unique: true
417
+ validates :email, uniqueness: true
418
+ ```
419
+
420
+ ### 2. Presence on Boolean Fields
421
+
422
+ ```ruby
423
+ # WRONG - false.blank? == true
424
+ validates :active, presence: true
425
+
426
+ # CORRECT
427
+ validates :active, inclusion: { in: [true, false] }
428
+ ```
429
+
430
+ ### 3. Format With ^ and $
431
+
432
+ ```ruby
433
+ # WRONG - vulnerable to multiline injection
434
+ validates :code, format: { with: /^[a-z]+$/ }
435
+
436
+ # CORRECT
437
+ validates :code, format: { with: /\A[a-z]+\z/ }
438
+ ```
439
+
440
+ ### 4. Bidirectional validates_associated
441
+
442
+ ```ruby
443
+ # WRONG - infinite loop
444
+ class Author < ApplicationRecord
445
+ has_many :books
446
+ validates_associated :books
447
+ end
448
+
449
+ class Book < ApplicationRecord
450
+ belongs_to :author
451
+ validates_associated :author # Don't do this!
452
+ end
453
+
454
+ # CORRECT - validate in one direction only
455
+ class Author < ApplicationRecord
456
+ has_many :books
457
+ validates_associated :books
458
+ end
459
+
460
+ class Book < ApplicationRecord
461
+ belongs_to :author
462
+ validates :author, presence: true
463
+ end
464
+ ```
465
+
466
+ ### 5. Confirmation Without Presence
467
+
468
+ ```ruby
469
+ # WRONG - nil confirmation always passes
470
+ validates :password, confirmation: true
471
+
472
+ # CORRECT
473
+ validates :password, confirmation: true
474
+ validates :password_confirmation, presence: true, if: :password_changed?
475
+ ```
476
+
477
+ ### 6. Overusing Conditionals
478
+
479
+ ```ruby
480
+ # WRONG - hard to follow
481
+ validates :field1, presence: true, if: :condition_a?
482
+ validates :field1, length: { minimum: 5 }, if: :condition_b?
483
+ validates :field2, presence: true, if: :condition_a?
484
+ validates :field2, format: { with: /.../ }, unless: :condition_c?
485
+
486
+ # BETTER - use validation contexts
487
+ validates :field1, :field2, presence: true, on: :step_one
488
+ validates :field1, length: { minimum: 5 }, on: :step_two
489
+
490
+ # Or use form objects for complex multi-step forms
491
+ ```
492
+
493
+ ### 7. Validating Auto-Generated Fields
494
+
495
+ ```ruby
496
+ # WRONG - validates internal implementation
497
+ validates :encrypted_password, presence: true
498
+ validates :uuid, format: { with: UUID_REGEX }
499
+
500
+ # CORRECT - validate user-provided input
501
+ validates :password, presence: true, on: :create
502
+ # Let the model generate uuid internally
503
+ ```
504
+
505
+ ## Validation Callbacks
506
+
507
+ ### before_validation
508
+
509
+ ```ruby
510
+ class User < ApplicationRecord
511
+ before_validation :normalize_email
512
+
513
+ private
514
+
515
+ def normalize_email
516
+ self.email = email&.downcase&.strip
517
+ end
518
+ end
519
+ ```
520
+
521
+ **Note**: `before_validation` can throw `:abort` to halt, but `validate` methods cannot halt the chain.
522
+
523
+ ### Halting Validation
524
+
525
+ ```ruby
526
+ before_validation :check_preconditions
527
+
528
+ private
529
+
530
+ def check_preconditions
531
+ throw(:abort) if skip_validation_flag
532
+ end
533
+ ```
534
+
535
+ ## Error Messages
536
+
537
+ ### Customizing Messages
538
+
539
+ ```ruby
540
+ validates :name, presence: { message: "is required" }
541
+ validates :age, numericality: { message: "must be a number" }
542
+ validates :size, inclusion: { in: %w[S M L], message: "%{value} is not a valid size" }
543
+ ```
544
+
545
+ ### Available Interpolations
546
+
547
+ | Variable | Description |
548
+ |----------|-------------|
549
+ | `%{value}` | Current attribute value |
550
+ | `%{attribute}` | Attribute name |
551
+ | `%{model}` | Model name |
552
+ | `%{count}` | Length/size constraint |
553
+
554
+ ### errors API
555
+
556
+ ```ruby
557
+ user.valid? # Run validations, return boolean
558
+ user.invalid? # Inverse of valid?
559
+ user.errors # ActiveModel::Errors object
560
+ user.errors[:email] # Array of errors for :email
561
+ user.errors.full_messages # ["Email can't be blank", "Email is invalid"]
562
+ user.errors.add(:base, "...") # Add error not tied to attribute
563
+ user.errors.clear # Remove all errors
564
+ ```
565
+
566
+ ## Performance Tips
567
+
568
+ ### Avoid N+1 in Uniqueness
569
+
570
+ ```ruby
571
+ # Potentially slow - queries DB on every update
572
+ validates :email, uniqueness: true
573
+
574
+ # Better - only check on create if email can't change
575
+ validates :email, uniqueness: true, on: :create
576
+ ```
577
+
578
+ ### Use Scoped Uniqueness
579
+
580
+ ```ruby
581
+ # Scoped queries are faster with proper indexes
582
+ validates :slug, uniqueness: { scope: :account_id }
583
+
584
+ # Migration
585
+ add_index :articles, [:account_id, :slug], unique: true
586
+ ```
587
+
588
+ ### Prefer Database for Heavy Validations
589
+
590
+ ```ruby
591
+ # Complex validation in Ruby - runs in app
592
+ validate :complex_business_rule
593
+
594
+ # Better for simple checks - use DB constraints
595
+ add_check_constraint :orders, "total >= 0", name: "orders_total_positive"
596
+ ```