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,709 @@
1
+ # ActiveRecord Associations Reference
2
+
3
+ ## Association Types Overview
4
+
5
+ ### belongs_to - Child Side
6
+
7
+ The declaring model contains the foreign key. Use singular form.
8
+
9
+ ```ruby
10
+ class Book < ApplicationRecord
11
+ belongs_to :author # Required by default (Rails 5+)
12
+ belongs_to :publisher, optional: true # Allow NULL foreign key
13
+ belongs_to :category, class_name: "Genre", foreign_key: "genre_id"
14
+ end
15
+ ```
16
+
17
+ **Key Options:**
18
+ - `optional: true` - Allow NULL foreign key
19
+ - `counter_cache: true` - Maintain count on parent (column goes on parent)
20
+ - `touch: true` - Update parent's `updated_at` on save
21
+ - `inverse_of: :association` - Bi-directional reference (required with custom FK)
22
+ - `polymorphic: true` - Belong to multiple model types
23
+
24
+ **Migration:**
25
+ ```ruby
26
+ create_table :books do |t|
27
+ t.belongs_to :author, null: false, foreign_key: true
28
+ end
29
+ ```
30
+
31
+ ---
32
+
33
+ ### has_one - Parent Side One-to-One
34
+
35
+ Another model has a reference to this model. Foreign key is on the other table.
36
+
37
+ ```ruby
38
+ class Supplier < ApplicationRecord
39
+ has_one :account, dependent: :destroy
40
+ has_one :representative, class_name: "Person"
41
+ end
42
+ ```
43
+
44
+ **Critical Warning:** Rails does NOT enforce 1:1 at database level. Add unique index:
45
+
46
+ ```ruby
47
+ # Migration - enforce true 1:1
48
+ add_index :accounts, :supplier_id, unique: true
49
+ ```
50
+
51
+ **Key Options:**
52
+ - `dependent: :destroy` - Destroy associated when parent destroyed
53
+ - `as: :attachable` - Polymorphic target
54
+ - `through: :other_association` - Through another association
55
+
56
+ ---
57
+
58
+ ### has_many - One-to-Many
59
+
60
+ This model has multiple instances of another model.
61
+
62
+ ```ruby
63
+ class Author < ApplicationRecord
64
+ has_many :books, dependent: :destroy
65
+ has_many :published_books, -> { where(published: true) }, class_name: "Book"
66
+ has_many :chapters, through: :books
67
+ end
68
+ ```
69
+
70
+ **Key Options:**
71
+ - `dependent:` - Cascade behavior (see Dependent Options below)
72
+ - `counter_cache:` - Custom column name for count
73
+ - `inverse_of:` - Bi-directional reference
74
+ - `through:` - Many-to-many via join model
75
+
76
+ **Collection Methods:**
77
+ ```ruby
78
+ author.books # Returns Relation
79
+ author.books.build # Create unsaved
80
+ author.books.create # Create and save
81
+ author.books << book # Add (saves immediately!)
82
+ author.book_ids # Array of IDs
83
+ ```
84
+
85
+ ---
86
+
87
+ ### has_and_belongs_to_many - Direct Many-to-Many
88
+
89
+ Simple many-to-many without join model. **Prefer `has_many :through` instead.**
90
+
91
+ ```ruby
92
+ class Assembly < ApplicationRecord
93
+ has_and_belongs_to_many :parts
94
+ end
95
+
96
+ class Part < ApplicationRecord
97
+ has_and_belongs_to_many :assemblies
98
+ end
99
+ ```
100
+
101
+ **Migration (no primary key):**
102
+ ```ruby
103
+ create_table :assemblies_parts, id: false do |t|
104
+ t.belongs_to :assembly, foreign_key: true
105
+ t.belongs_to :part, foreign_key: true
106
+ end
107
+ add_index :assemblies_parts, [:assembly_id, :part_id], unique: true
108
+ ```
109
+
110
+ **Join Table Naming:** Lexically ordered - `papers_paper_boxes` not `paper_boxes_papers`. Use explicit `:join_table` to avoid surprises.
111
+
112
+ **Critical:** Declare `has_and_belongs_to_many` BEFORE `self.table_name =` in model.
113
+
114
+ ---
115
+
116
+ ## Through Associations
117
+
118
+ ### has_many :through
119
+
120
+ Many-to-many with join model. **Always prefer over HABTM.**
121
+
122
+ ```
123
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
124
+ │ Physician │ │ Appointment │ │ Patient │
125
+ ├──────────────┤ ├──────────────┤ ├──────────────┤
126
+ │ id │◄──────│ physician_id │ │ id │
127
+ │ name │ │ patient_id │──────►│ name │
128
+ │ │ │ scheduled_at │ │ │
129
+ └──────────────┘ └──────────────┘ └──────────────┘
130
+ │ ▲ │
131
+ │ has_many │ has_many │
132
+ └─────:through────────┴──────:through───────┘
133
+ ```
134
+
135
+ ```ruby
136
+ class Physician < ApplicationRecord
137
+ has_many :appointments
138
+ has_many :patients, through: :appointments
139
+ end
140
+
141
+ class Appointment < ApplicationRecord
142
+ belongs_to :physician
143
+ belongs_to :patient
144
+
145
+ # Join model can have attributes and validations
146
+ validates :scheduled_at, presence: true
147
+ end
148
+
149
+ class Patient < ApplicationRecord
150
+ has_many :appointments
151
+ has_many :physicians, through: :appointments
152
+ end
153
+ ```
154
+
155
+ **Advantages over HABTM:**
156
+ - Add attributes/validations to join model
157
+ - Store metadata (timestamps, status)
158
+ - Add callbacks to relationship changes
159
+ - Easier to extend later
160
+
161
+ ---
162
+
163
+ ### has_one :through
164
+
165
+ Access single record through intermediate association.
166
+
167
+ ```
168
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
169
+ │ Supplier │ │ Account │ │ AccountHistory│
170
+ ├──────────────┤ ├──────────────┤ ├──────────────┤
171
+ │ id │◄──────│ supplier_id │ │ id │
172
+ │ name │ │ id │◄──────│ account_id │
173
+ │ │ │ │ │ credit_rating│
174
+ └──────────────┘ └──────────────┘ └──────────────┘
175
+ │ │
176
+ │ has_one :through │
177
+ └─────────────────────┘
178
+ ```
179
+
180
+ ```ruby
181
+ class Supplier < ApplicationRecord
182
+ has_one :account
183
+ has_one :account_history, through: :account
184
+ end
185
+
186
+ class Account < ApplicationRecord
187
+ belongs_to :supplier
188
+ has_one :account_history
189
+ end
190
+
191
+ class AccountHistory < ApplicationRecord
192
+ belongs_to :account
193
+ end
194
+ ```
195
+
196
+ ---
197
+
198
+ ### Through Association Writability
199
+
200
+ **Critical Rule:** `:through` associations are only writable when join model uses `belongs_to`.
201
+
202
+ ```ruby
203
+ # WORKS - join model has belongs_to
204
+ class Tagging < ApplicationRecord
205
+ belongs_to :post
206
+ belongs_to :tag
207
+ end
208
+ post.tags << tag # Creates Tagging record
209
+
210
+ # READ-ONLY - join model has has_one/has_many
211
+ class Group < ApplicationRecord
212
+ has_many :users
213
+ has_many :avatars, through: :users # Read-only!
214
+ end
215
+ group.avatars << avatar # WON'T WORK
216
+ ```
217
+
218
+ **Solution:** Manipulate the `:through` association directly.
219
+
220
+ ---
221
+
222
+ ## Polymorphic Associations
223
+
224
+ Single association points to multiple model types.
225
+
226
+ ```
227
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
228
+ │ Employee │ │ Picture │ │ Product │
229
+ ├──────────────┤ ├──────────────┤ ├──────────────┤
230
+ │ id │◄──┐ │ id │ ┌──►│ id │
231
+ │ name │ │ │ imageable_type│ │ │ name │
232
+ │ │ └───│ imageable_id │───┘ │ │
233
+ └──────────────┘ │ name │ └──────────────┘
234
+ └──────────────┘
235
+ ```
236
+
237
+ ```ruby
238
+ class Picture < ApplicationRecord
239
+ belongs_to :imageable, polymorphic: true
240
+ end
241
+
242
+ class Employee < ApplicationRecord
243
+ has_many :pictures, as: :imageable
244
+ end
245
+
246
+ class Product < ApplicationRecord
247
+ has_many :pictures, as: :imageable
248
+ end
249
+ ```
250
+
251
+ **Migration:**
252
+ ```ruby
253
+ create_table :pictures do |t|
254
+ t.string :name
255
+ t.belongs_to :imageable, polymorphic: true
256
+ t.timestamps
257
+ end
258
+ # Creates: imageable_type (string), imageable_id (integer)
259
+ ```
260
+
261
+ **Naming Convention:**
262
+ - Use `-able` suffix when association is recipient: `imageable`, `attachable`, `commentable`
263
+ - Use subject form when acting: `author` not `authorable`
264
+
265
+ **STI Compatibility Warning:**
266
+ When using polymorphic with STI, store base class in type column:
267
+
268
+ ```ruby
269
+ class Asset < ApplicationRecord
270
+ belongs_to :attachable, polymorphic: true
271
+
272
+ def attachable_type=(class_name)
273
+ super(class_name.constantize.base_class.to_s)
274
+ end
275
+ end
276
+ ```
277
+
278
+ **Limitations:**
279
+ - Cannot use database foreign key constraints
280
+ - Cannot use `joins`, only `includes` for eager loading
281
+ - When renaming models, must update `*_type` column values
282
+
283
+ ---
284
+
285
+ ## Self-Referential Associations
286
+
287
+ Model relates to itself (hierarchies, trees, graphs).
288
+
289
+ ```ruby
290
+ class Employee < ApplicationRecord
291
+ # Employee has one manager (who is also an Employee)
292
+ belongs_to :manager, class_name: "Employee", optional: true
293
+
294
+ # Employee has many subordinates (who are also Employees)
295
+ has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
296
+ end
297
+
298
+ # Usage
299
+ employee.manager # Returns manager Employee
300
+ employee.subordinates # Returns Employee collection
301
+ ```
302
+
303
+ **Migration:**
304
+ ```ruby
305
+ create_table :employees do |t|
306
+ t.string :name
307
+ t.belongs_to :manager, foreign_key: { to_table: :employees }
308
+ t.timestamps
309
+ end
310
+ ```
311
+
312
+ **Friendship Example (Many-to-Many Self-Join):**
313
+ ```ruby
314
+ class User < ApplicationRecord
315
+ has_many :friendships
316
+ has_many :friends, through: :friendships
317
+ end
318
+
319
+ class Friendship < ApplicationRecord
320
+ belongs_to :user
321
+ belongs_to :friend, class_name: "User"
322
+ end
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Critical Association Options
328
+
329
+ ### inverse_of - Bi-directional References
330
+
331
+ Tells Rails two associations represent same relationship from different sides.
332
+
333
+ **When Required:**
334
+ 1. With custom `:foreign_key`
335
+ 2. On `:through` join model associations
336
+ 3. With `accepts_nested_attributes_for`
337
+
338
+ ```ruby
339
+ class Author < ApplicationRecord
340
+ has_many :books, inverse_of: :author
341
+ end
342
+
343
+ class Book < ApplicationRecord
344
+ belongs_to :author, inverse_of: :books
345
+ end
346
+ ```
347
+
348
+ **Why It Matters:**
349
+
350
+ ```ruby
351
+ # WITHOUT inverse_of - extra query
352
+ author = Author.first
353
+ book = author.books.first
354
+ book.author.name # Queries DB again!
355
+
356
+ # WITH inverse_of - no extra query
357
+ book.author.object_id == author.object_id # Same object
358
+ ```
359
+
360
+ **accepts_nested_attributes_for Requirement:**
361
+
362
+ ```ruby
363
+ class Notice < ApplicationRecord
364
+ has_many :entity_roles, inverse_of: :notice # REQUIRED!
365
+ accepts_nested_attributes_for :entity_roles
366
+ end
367
+
368
+ class EntityRole < ApplicationRecord
369
+ belongs_to :notice
370
+ validates :notice, presence: true # Fails without inverse_of
371
+ end
372
+ ```
373
+
374
+ Without `inverse_of`, Rails can't assign parent before validation, causing "can't be blank" errors.
375
+
376
+ **Automatic Detection Limitations:**
377
+ Rails auto-detects `:inverse_of` for simple associations but NOT when using:
378
+ - `:foreign_key` option
379
+ - `:through` option
380
+ - Custom scopes
381
+ - Non-standard naming
382
+
383
+ **Best Practice:** Always set `inverse_of` when using custom `:foreign_key`.
384
+
385
+ ---
386
+
387
+ ### dependent - Cascade Deletion
388
+
389
+ Controls what happens to associated records when parent is destroyed.
390
+
391
+ | Option | Behavior | Callbacks | Speed |
392
+ |--------|----------|-----------|-------|
393
+ | `:destroy` | Destroy each record | Yes | Slow |
394
+ | `:delete_all` | SQL DELETE (no load) | No | Fast |
395
+ | `:nullify` | Set FK to NULL | No | Fast |
396
+ | `:restrict_with_exception` | Raise if any exist | N/A | N/A |
397
+ | `:restrict_with_error` | Add error if any exist | N/A | N/A |
398
+ | `:destroy_async` | Background job destroy | Yes | Async |
399
+
400
+ **Decision Matrix:**
401
+
402
+ | Use Case | Option |
403
+ |----------|--------|
404
+ | Standard cascade | `:destroy` |
405
+ | No child callbacks needed | `:delete_all` |
406
+ | Keep orphan records | `:nullify` |
407
+ | Prevent accidental deletion | `:restrict_with_exception` |
408
+ | Large-scale deletions | `:destroy_async` |
409
+
410
+ **Warning - delete_all Breaks Grandchildren:**
411
+
412
+ ```ruby
413
+ class Parent < ApplicationRecord
414
+ has_many :children, dependent: :delete_all
415
+ end
416
+
417
+ class Child < ApplicationRecord
418
+ has_many :grandchildren, dependent: :destroy
419
+ end
420
+
421
+ parent.destroy # Deletes children, ORPHANS grandchildren!
422
+ ```
423
+
424
+ **Warning - destroy_async + FK Constraints:**
425
+ Do NOT use `:destroy_async` with database foreign key constraints. FK actions occur in same transaction, but async job runs later causing violations.
426
+
427
+ **Orphan-Then-Purge Pattern (Large Datasets):**
428
+
429
+ ```ruby
430
+ class Blog < ApplicationRecord
431
+ has_many :posts, dependent: :nullify # Fast orphaning
432
+ end
433
+
434
+ # Background job cleans up
435
+ Post.where(blog_id: nil).find_each(&:destroy) # Proper callbacks
436
+ ```
437
+
438
+ **Scoped Association Warning:**
439
+
440
+ ```ruby
441
+ has_many :comments, -> { where(published: true) }, dependent: :destroy
442
+ ```
443
+ Only published comments destroyed - unpublished become orphans!
444
+
445
+ ---
446
+
447
+ ### counter_cache - Count Optimization
448
+
449
+ Caches association count to eliminate COUNT queries.
450
+
451
+ ```ruby
452
+ class Comment < ApplicationRecord
453
+ belongs_to :post, counter_cache: true
454
+ end
455
+
456
+ class Post < ApplicationRecord
457
+ has_many :comments
458
+ attr_readonly :comments_count # Prevent manual updates
459
+ end
460
+ ```
461
+
462
+ **Migration:**
463
+ ```ruby
464
+ add_column :posts, :comments_count, :integer, default: 0, null: false
465
+ ```
466
+
467
+ **Backfilling Existing Data:**
468
+
469
+ ```ruby
470
+ # Option 1: Simple reset
471
+ Post.find_each { |post| Post.reset_counters(post.id, :comments) }
472
+
473
+ # Option 2: For large tables, disable during backfill
474
+ belongs_to :post, counter_cache: { active: false }
475
+ # After backfill complete, change to:
476
+ belongs_to :post, counter_cache: true
477
+ ```
478
+
479
+ **Custom Column Name:**
480
+ ```ruby
481
+ belongs_to :post, counter_cache: :my_comments_count
482
+ ```
483
+
484
+ **Gotchas:**
485
+ - Only updates via callbacks (`.delete` bypasses)
486
+ - Doesn't support scoped counts (use `counter_culture` gem)
487
+ - Consider database triggers for high-write scenarios
488
+
489
+ ---
490
+
491
+ ### autosave - Automatic Associated Saving
492
+
493
+ Controls when associated records are saved with parent.
494
+
495
+ | Setting | Behavior |
496
+ |---------|----------|
497
+ | Not specified | Save new records only |
498
+ | `true` | Save new AND updated records |
499
+ | `false` | Never auto-save |
500
+
501
+ ```ruby
502
+ class Author < ApplicationRecord
503
+ has_one :profile, autosave: true
504
+ end
505
+
506
+ author = Author.new(name: "Jane")
507
+ author.build_profile(bio: "Writer")
508
+ author.save # Saves both author AND profile
509
+ ```
510
+
511
+ **accepts_nested_attributes_for Auto-Enables:**
512
+ ```ruby
513
+ accepts_nested_attributes_for :books # Sets autosave: true automatically
514
+ ```
515
+
516
+ **Callback Order Warning:**
517
+ Autosave defines callbacks. Define associations BEFORE custom callbacks:
518
+
519
+ ```ruby
520
+ class Author < ApplicationRecord
521
+ has_many :books, autosave: true # First
522
+ before_save :do_something # Second - runs after autosave setup
523
+ end
524
+ ```
525
+
526
+ ---
527
+
528
+ ## Association Extensions
529
+
530
+ Add custom methods to association proxies.
531
+
532
+ ```ruby
533
+ class Project < ApplicationRecord
534
+ has_many :tasks do
535
+ def active
536
+ where(status: 'active')
537
+ end
538
+
539
+ def by_priority
540
+ order(priority: :desc)
541
+ end
542
+
543
+ def total_hours
544
+ sum(:estimated_hours)
545
+ end
546
+ end
547
+ end
548
+
549
+ # Usage
550
+ project.tasks.active.by_priority
551
+ project.tasks.total_hours
552
+ ```
553
+
554
+ **Shared Extensions via Module:**
555
+
556
+ ```ruby
557
+ module StatusFilter
558
+ def active
559
+ where(status: 'active')
560
+ end
561
+
562
+ def completed
563
+ where(status: 'completed')
564
+ end
565
+ end
566
+
567
+ class Project < ApplicationRecord
568
+ has_many :tasks, -> { extending StatusFilter }
569
+ has_many :milestones, -> { extending StatusFilter }
570
+ end
571
+ ```
572
+
573
+ **Accessing Parent Object:**
574
+ ```ruby
575
+ has_many :tasks do
576
+ def recent_for_owner
577
+ where("created_at > ?", proxy_association.owner.created_at)
578
+ end
579
+ end
580
+ ```
581
+
582
+ ---
583
+
584
+ ## N+1 Query Prevention
585
+
586
+ ### Eager Loading Methods
587
+
588
+ | Method | Strategy | Use When |
589
+ |--------|----------|----------|
590
+ | `includes` | Auto-choose | Default choice |
591
+ | `preload` | Separate queries | Large datasets, no filtering |
592
+ | `eager_load` | LEFT OUTER JOIN | Filtering/sorting by association |
593
+ | `joins` | INNER JOIN | Filtering only, not accessing data |
594
+
595
+ **Examples:**
596
+
597
+ ```ruby
598
+ # includes - smart default (Rails decides preload vs eager_load)
599
+ Author.includes(:books).each { |a| a.books.size }
600
+
601
+ # preload - always separate queries
602
+ Author.preload(:books).limit(10) # 2 queries total
603
+
604
+ # eager_load - always single JOIN
605
+ Author.eager_load(:books).where(books: { published: true })
606
+
607
+ # joins - filtering only (doesn't load association)
608
+ Author.joins(:books).where(books: { published: true }).distinct
609
+ ```
610
+
611
+ **Strict Loading (Rails 6.1+):**
612
+ ```ruby
613
+ Author.strict_loading.first
614
+ author.books # Raises StrictLoadingViolationError!
615
+
616
+ # Per-association
617
+ has_many :books, strict_loading: true
618
+ ```
619
+
620
+ **Nested Eager Loading:**
621
+ ```ruby
622
+ Author.includes(books: :publisher)
623
+ Author.includes(books: [:publisher, :reviews])
624
+ Author.includes(books: { publisher: :address })
625
+ ```
626
+
627
+ ---
628
+
629
+ ## Anti-Patterns
630
+
631
+ ### 1. Naming After AR Methods
632
+ ```ruby
633
+ # BAD - conflicts with AR::Base methods
634
+ has_many :attributes
635
+ has_one :connection
636
+ ```
637
+
638
+ ### 2. Relying on Validations for Uniqueness
639
+ ```ruby
640
+ # BAD - race condition
641
+ validates :supplier, uniqueness: true
642
+
643
+ # GOOD - database constraint
644
+ add_index :accounts, :supplier_id, unique: true
645
+ ```
646
+
647
+ ### 3. Callbacks for Business Logic
648
+ ```ruby
649
+ # BAD - hidden side effects
650
+ after_create :send_email
651
+ after_update :sync_to_api
652
+
653
+ # GOOD - explicit service object
654
+ class ProjectCreator
655
+ def call(params)
656
+ project = Project.create!(params)
657
+ ProjectMailer.created(project).deliver_later
658
+ ExternalApi.sync(project)
659
+ project
660
+ end
661
+ end
662
+ ```
663
+
664
+ ### 4. HABTM Without Future Consideration
665
+ ```ruby
666
+ # BAD - hard to extend later
667
+ has_and_belongs_to_many :tags
668
+
669
+ # GOOD - flexible from start
670
+ has_many :taggings
671
+ has_many :tags, through: :taggings
672
+ ```
673
+
674
+ ### 5. Missing inverse_of with Custom FK
675
+ ```ruby
676
+ # BAD - causes extra queries, validation failures
677
+ belongs_to :author, foreign_key: "writer_id"
678
+
679
+ # GOOD
680
+ belongs_to :author, foreign_key: "writer_id", inverse_of: :books
681
+ ```
682
+
683
+ ### 6. Eager Loading with Limit
684
+ ```ruby
685
+ # WARNING - limit ignored with includes!
686
+ has_many :recent_comments, -> { order(created_at: :desc).limit(5) }
687
+ Post.includes(:recent_comments).first.recent_comments # Returns ALL comments!
688
+ ```
689
+
690
+ ---
691
+
692
+ ## Naming Conventions
693
+
694
+ Follow GitLab style guide for scope naming:
695
+
696
+ | Pattern | Purpose | Example |
697
+ |---------|---------|---------|
698
+ | `for_*` | Filter by belongs_to | `scope :for_user, ->(u) { where(user: u) }` |
699
+ | `with_*` | Joins/eager load or filter has_* | `scope :with_comments, -> { joins(:comments) }` |
700
+ | `order_by_*` | Ordering | `scope :order_by_recent, -> { order(created_at: :desc) }` |
701
+
702
+ ---
703
+
704
+ ## See Also
705
+
706
+ - `examples/associations/` - Working code examples
707
+ - `references/querying.md` - Eager loading details
708
+ - `references/callbacks.md` - Callback alternatives
709
+ - `references/migrations.md` - Foreign key constraints