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,255 @@
1
+ ---
2
+ name: activerecord
3
+ description: "ActiveRecord patterns — associations, validations, queries, migrations, eager loading. Activate when working with models, migrations, database schema, N+1 queries, scopes, includes/preload/eager_load, callbacks, or editing files in app/models/ or db/migrate/."
4
+ ---
5
+
6
+ # ActiveRecord
7
+
8
+ This skill provides comprehensive guidance for working with ActiveRecord in Rails applications. Use for writing migrations, defining associations, optimizing queries, preventing N+1 issues, implementing validations, and following database best practices.
9
+
10
+ ## Quick Reference
11
+
12
+ ### CRUD Operations
13
+
14
+ ```ruby
15
+ # Create
16
+ user = User.create(name: "Alice", email: "alice@example.com")
17
+ user = User.create!(...) # Raises on failure
18
+
19
+ # Read
20
+ User.find(1) # Raises RecordNotFound
21
+ User.find_by(email: "x") # Returns nil if not found
22
+ User.where(active: true) # Returns Relation
23
+
24
+ # Update
25
+ user.update(name: "Bob")
26
+ user.update!(...) # Raises on failure
27
+
28
+ # Delete (callbacks run)
29
+ user.destroy
30
+
31
+ # Delete (no callbacks)
32
+ user.delete
33
+ ```
34
+
35
+ ### Key Concepts
36
+
37
+ | Concept | Purpose |
38
+ |---------|---------|
39
+ | `belongs_to` | Child side of association (has foreign key) |
40
+ | `has_many` / `has_one` | Parent side of association |
41
+ | `has_many :through` | Many-to-many via join model |
42
+ | `includes` / `preload` | Eager loading (prevent N+1) |
43
+ | `scope` | Named query builder |
44
+ | `validates` | Model-level data validation |
45
+ | `before_save` / `after_commit` | Lifecycle callbacks |
46
+
47
+ ## Eager Loading Decision Tree
48
+
49
+ ```
50
+ Need to access associated data?
51
+ ├── NO → Use `joins` (filtering only)
52
+ └── YES → Need to filter/sort by association?
53
+ ├── NO → Use `preload` (separate queries)
54
+ └── YES → Large dataset with many associations?
55
+ ├── YES → Use `includes` with `references`
56
+ └── NO → Use `eager_load` (single JOIN)
57
+ ```
58
+
59
+ ### Quick Comparison
60
+
61
+ | Method | Strategy | Best For |
62
+ |--------|----------|----------|
63
+ | `includes` | Auto-choose | Default choice |
64
+ | `preload` | Separate queries | Large datasets, no filtering |
65
+ | `eager_load` | LEFT OUTER JOIN | Filtering by association |
66
+ | `joins` | INNER JOIN | Filtering only, not accessing data |
67
+
68
+ ```ruby
69
+ # N+1 problem
70
+ Post.all.each { |p| p.author.name } # 1 + N queries
71
+
72
+ # Solution
73
+ Post.includes(:author).each { |p| p.author.name } # 2 queries
74
+ ```
75
+
76
+ ## Validation vs Constraint Decision
77
+
78
+ ```
79
+ Does the rule ALWAYS apply, regardless of business logic?
80
+ ├── Yes → Database constraint
81
+ │ └── Examples: NOT NULL, foreign keys, unique emails
82
+ └── No → Model validation
83
+ └── Examples: Format rules that change, conditional requirements
84
+
85
+ Need helpful user-facing error messages?
86
+ ├── Yes → Model validation (possibly WITH constraint)
87
+ └── No → Constraint alone is fine
88
+ ```
89
+
90
+ **Best Practice**: Use both for critical fields:
91
+
92
+ ```ruby
93
+ # Migration (data integrity)
94
+ add_index :users, :email, unique: true
95
+
96
+ # Model (user feedback)
97
+ validates :email, presence: true, uniqueness: true
98
+ ```
99
+
100
+ ## Associations Quick Reference
101
+
102
+ ### Basic Types
103
+
104
+ ```ruby
105
+ class Author < ApplicationRecord
106
+ has_many :books, dependent: :destroy
107
+ has_one :profile
108
+ end
109
+
110
+ class Book < ApplicationRecord
111
+ belongs_to :author # Required by default
112
+ belongs_to :publisher, optional: true # Allow NULL
113
+ end
114
+ ```
115
+
116
+ ### Through Associations
117
+
118
+ ```ruby
119
+ class Physician < ApplicationRecord
120
+ has_many :appointments
121
+ has_many :patients, through: :appointments
122
+ end
123
+
124
+ class Appointment < ApplicationRecord
125
+ belongs_to :physician
126
+ belongs_to :patient
127
+ # Join model can have attributes
128
+ validates :scheduled_at, presence: true
129
+ end
130
+ ```
131
+
132
+ ### Critical Options
133
+
134
+ | Option | Purpose |
135
+ |--------|---------|
136
+ | `inverse_of` | Required with custom foreign_key |
137
+ | `dependent: :destroy` | Cascade delete with callbacks |
138
+ | `counter_cache: true` | Cache association count |
139
+ | `touch: true` | Update parent's updated_at |
140
+
141
+ ## Migrations Quick Reference
142
+
143
+ ### Safe Patterns
144
+
145
+ ```ruby
146
+ # Always reversible
147
+ add_column :users, :name, :string
148
+ add_index :users, :email, unique: true
149
+ add_reference :orders, :user, foreign_key: true
150
+
151
+ # Concurrent index (no table lock)
152
+ disable_ddl_transaction!
153
+ add_index :users, :email, algorithm: :concurrently
154
+ ```
155
+
156
+ ### Must Include Type for Reversibility
157
+
158
+ ```ruby
159
+ remove_column :users, :legacy_field, :string # Include type!
160
+ change_column_default :users, :status, from: nil, to: "active"
161
+ ```
162
+
163
+ ## Callbacks Quick Reference
164
+
165
+ ### Order of Execution
166
+
167
+ ```
168
+ before_validation → after_validation →
169
+ before_save → around_save → before_create →
170
+ around_create → [INSERT] → after_create →
171
+ after_save → [COMMIT] → after_commit
172
+ ```
173
+
174
+ ### Critical Rule: Use after_commit for External Systems
175
+
176
+ ```ruby
177
+ # WRONG - Race condition!
178
+ after_save :enqueue_processing
179
+
180
+ # CORRECT - Runs after COMMIT
181
+ after_commit :enqueue_processing, on: :create
182
+ ```
183
+
184
+ ## Batch Processing
185
+
186
+ ```ruby
187
+ # BAD - loads all records
188
+ User.all.each { |u| process(u) }
189
+
190
+ # GOOD - processes in batches
191
+ User.find_each { |u| process(u) }
192
+
193
+ # Bulk operations
194
+ User.where(old: true).in_batches.update_all(archived: true)
195
+ ```
196
+
197
+ ## Best Practices
198
+
199
+ ### Do
200
+
201
+ - Add database index for columns used in WHERE, ORDER BY, JOIN
202
+ - Use `includes` to prevent N+1 queries
203
+ - Pair uniqueness validations with unique database indexes
204
+ - Use `find_each` for processing large datasets
205
+ - Use `pluck(:column)` instead of `all.map(&:column)`
206
+ - Define `inverse_of` when using custom `foreign_key`
207
+ - Use `after_commit` for background jobs and external APIs
208
+ - Prefer `has_many :through` over `has_and_belongs_to_many`
209
+
210
+ ### Don't
211
+
212
+ - Don't use `default_scope` (causes subtle issues)
213
+ - Don't use `delete` when you need callbacks
214
+ - Don't skip database constraints for critical uniqueness
215
+ - Don't use `update_column` to bypass validations casually
216
+ - Don't reference models in migrations (use raw SQL)
217
+ - Don't edit already-deployed migrations
218
+ - Don't use `after_save` for external system interactions
219
+
220
+ ## Anti-Patterns Quick List
221
+
222
+ | Anti-Pattern | Solution |
223
+ |--------------|----------|
224
+ | N+1 queries | Use `includes`, `preload`, or `eager_load` |
225
+ | `User.all.map(&:email)` | Use `User.pluck(:email)` |
226
+ | Uniqueness without index | Add unique database index |
227
+ | `validates :active, presence: true` | Use `inclusion: { in: [true, false] }` for booleans |
228
+ | `after_save` for jobs | Use `after_commit` |
229
+ | Callback hell | Extract to service objects |
230
+ | `default_scope` | Use explicit scopes |
231
+ | `has_and_belongs_to_many` | Use `has_many :through` |
232
+
233
+ ## Additional Resources
234
+
235
+ ### Reference Files
236
+
237
+ For detailed patterns and complete API references, consult:
238
+
239
+ - **`references/basics.md`** - Conventions, CRUD, dirty tracking, STI, type casting
240
+ - **`references/migrations.md`** - Schema changes, indexes, constraints, safe patterns
241
+ - **`references/validations.md`** - Built-in validators, custom validators, contexts
242
+ - **`references/callbacks.md`** - Lifecycle hooks, transaction callbacks, alternatives
243
+ - **`references/associations.md`** - All association types, inverse_of, dependent options
244
+ - **`references/querying.md`** - Finders, eager loading, scopes, batch processing
245
+
246
+ ### Example Files
247
+
248
+ Ready-to-use code patterns in `examples/`:
249
+
250
+ - **`examples/basics/`** - CRUD, dirty tracking, type casting, inheritance
251
+ - **`examples/migrations/`** - Schema changes, indexes, safe patterns, reversibility
252
+ - **`examples/validations/`** - Built-in, conditional, custom, contexts, constraints
253
+ - **`examples/callbacks/`** - Lifecycle, transaction callbacks, conditional, alternatives
254
+ - **`examples/associations/`** - Basic, through, polymorphic, self-referential, extensions
255
+ - **`examples/querying/`** - Finders, eager loading, scopes, batch processing, optimization
@@ -0,0 +1,298 @@
1
+ # Association Extension Examples
2
+ # Adding custom methods to association proxies
3
+
4
+ # ============================================
5
+ # Inline Extension
6
+ # ============================================
7
+
8
+ class Project < ApplicationRecord
9
+ has_many :tasks do
10
+ # Filter methods
11
+ def active
12
+ where(status: "active")
13
+ end
14
+
15
+ def completed
16
+ where(status: "completed")
17
+ end
18
+
19
+ def overdue
20
+ active.where("due_date < ?", Date.current)
21
+ end
22
+
23
+ # Calculation methods
24
+ def total_hours
25
+ sum(:estimated_hours)
26
+ end
27
+
28
+ def completion_percentage
29
+ return 0 if empty?
30
+
31
+ (completed.count.to_f / count * 100).round(2)
32
+ end
33
+
34
+ # Creation helpers
35
+ def create_milestone!(name, due_date)
36
+ create!(name:, due_date:, priority: :high, milestone: true)
37
+ end
38
+
39
+ # Access owner via proxy_association
40
+ def recent_for_owner
41
+ where("created_at > ?", proxy_association.owner.created_at)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Usage
47
+ project = Project.find(1)
48
+ project.tasks.active
49
+ project.tasks.overdue.count
50
+ project.tasks.total_hours
51
+ project.tasks.completion_percentage
52
+ project.tasks.create_milestone!("Launch", 1.month.from_now)
53
+
54
+ # ============================================
55
+ # Shared Extension Module
56
+ # ============================================
57
+
58
+ module StatusFilter
59
+ def active
60
+ where(status: "active")
61
+ end
62
+
63
+ def completed
64
+ where(status: "completed")
65
+ end
66
+
67
+ def pending
68
+ where(status: "pending")
69
+ end
70
+
71
+ def by_status(status)
72
+ where(status:)
73
+ end
74
+ end
75
+
76
+ module Orderable
77
+ def by_priority
78
+ order(priority: :desc)
79
+ end
80
+
81
+ def by_recent
82
+ order(created_at: :desc)
83
+ end
84
+
85
+ def by_due_date
86
+ order(due_date: :asc)
87
+ end
88
+ end
89
+
90
+ class Project < ApplicationRecord
91
+ has_many :tasks, -> { extending StatusFilter, Orderable }
92
+ has_many :issues, -> { extending StatusFilter, Orderable }
93
+ end
94
+
95
+ class Team < ApplicationRecord
96
+ has_many :members, -> { extending StatusFilter }
97
+ end
98
+
99
+ # Usage
100
+ project.tasks.active.by_priority
101
+ project.issues.pending.by_due_date
102
+ team.members.active
103
+
104
+ # ============================================
105
+ # Extension with Scopes
106
+ # ============================================
107
+
108
+ class Author < ApplicationRecord
109
+ has_many :books, -> { order(published_at: :desc) } do
110
+ def fiction
111
+ where(genre: "fiction")
112
+ end
113
+
114
+ def nonfiction
115
+ where(genre: "nonfiction")
116
+ end
117
+
118
+ def bestsellers
119
+ where(bestseller: true)
120
+ end
121
+
122
+ def published_in(year)
123
+ where("EXTRACT(YEAR FROM published_at) = ?", year)
124
+ end
125
+
126
+ def search(query)
127
+ where("title ILIKE ?", "%#{query}%")
128
+ end
129
+ end
130
+ end
131
+
132
+ # Chain extensions with other query methods
133
+ author.books.fiction.bestsellers
134
+ author.books.nonfiction.published_in(2024)
135
+ author.books.search("ruby").limit(10)
136
+
137
+ # ============================================
138
+ # Extension Accessing Owner
139
+ # ============================================
140
+
141
+ class User < ApplicationRecord
142
+ has_many :notifications do
143
+ def unread
144
+ where(read_at: nil)
145
+ end
146
+
147
+ def mark_all_read!
148
+ update_all(read_at: Time.current)
149
+ end
150
+
151
+ def for_past_week
152
+ where("created_at > ?", 1.week.ago)
153
+ end
154
+
155
+ # Access the user via proxy_association.owner
156
+ def summary
157
+ owner = proxy_association.owner
158
+ {
159
+ total: count,
160
+ unread: unread.count,
161
+ user_name: owner.name
162
+ }
163
+ end
164
+ end
165
+ end
166
+
167
+ # Usage
168
+ user.notifications.unread.count
169
+ user.notifications.mark_all_read!
170
+ user.notifications.summary
171
+ # => { total: 42, unread: 5, user_name: "Alice" }
172
+
173
+ # ============================================
174
+ # Extension for Through Associations
175
+ # ============================================
176
+
177
+ class Doctor < ApplicationRecord
178
+ has_many :appointments
179
+ has_many :patients, through: :appointments do
180
+ def with_recent_visit
181
+ where("appointments.scheduled_at > ?", 6.months.ago)
182
+ end
183
+
184
+ def needing_followup
185
+ joins(:appointments)
186
+ .where(appointments: { followup_needed: true })
187
+ .distinct
188
+ end
189
+ end
190
+ end
191
+
192
+ # Usage
193
+ doctor.patients.with_recent_visit
194
+ doctor.patients.needing_followup.count
195
+
196
+ # ============================================
197
+ # Real-World Example: Versioned Documents
198
+ # ============================================
199
+
200
+ class Document < ApplicationRecord
201
+ has_many :versions, -> { order(version_number: :desc) } do
202
+ def latest
203
+ first
204
+ end
205
+
206
+ def published
207
+ where(published: true)
208
+ end
209
+
210
+ def latest_published
211
+ published.first
212
+ end
213
+
214
+ def diff(v1, v2)
215
+ # Return diff between two versions
216
+ version1 = find_by(version_number: v1)
217
+ version2 = find_by(version_number: v2)
218
+ # Implementation...
219
+ end
220
+
221
+ def create_new_version!(content, author:)
222
+ last_number = maximum(:version_number) || 0
223
+ create!(
224
+ content:,
225
+ author:,
226
+ version_number: last_number + 1,
227
+ created_at: Time.current
228
+ )
229
+ end
230
+ end
231
+ end
232
+
233
+ # Usage
234
+ doc.versions.latest
235
+ doc.versions.published.count
236
+ doc.versions.create_new_version!("Updated content", author: current_user)
237
+
238
+ # ============================================
239
+ # Extension vs Scope Decision
240
+ # ============================================
241
+
242
+ # Use SCOPE when:
243
+ # - Query is useful globally (not just via association)
244
+ # - Simple filtering/ordering
245
+ # - Used in multiple associations
246
+
247
+ class Task < ApplicationRecord
248
+ scope :active, -> { where(status: "active") }
249
+ scope :by_priority, -> { order(priority: :desc) }
250
+ end
251
+
252
+ # Use EXTENSION when:
253
+ # - Query only makes sense in association context
254
+ # - Need access to owner (proxy_association.owner)
255
+ # - Doing mutations (mark_all_read!, etc.)
256
+ # - Complex business logic specific to relationship
257
+
258
+ class Project < ApplicationRecord
259
+ has_many :tasks do
260
+ # Makes sense only in project context
261
+ def critical_path
262
+ # Complex calculation involving project data
263
+ owner = proxy_association.owner
264
+ active.where("due_date <= ?", owner.deadline)
265
+ .order(priority: :desc)
266
+ end
267
+ end
268
+ end
269
+
270
+ # ============================================
271
+ # Testing Extensions
272
+ # ============================================
273
+
274
+ # spec/models/project_spec.rb
275
+ RSpec.describe Project do
276
+ describe "tasks extension" do
277
+ let(:project) { create(:project) }
278
+ let!(:active_task) { create(:task, project:, status: "active") }
279
+ let!(:completed_task) { create(:task, project:, status: "completed") }
280
+
281
+ describe "#active" do
282
+ it "returns only active tasks" do
283
+ expect(project.tasks.active).to contain_exactly(active_task)
284
+ end
285
+ end
286
+
287
+ describe "#completion_percentage" do
288
+ it "calculates correct percentage" do
289
+ expect(project.tasks.completion_percentage).to eq(50.0)
290
+ end
291
+
292
+ it "returns 0 for empty association" do
293
+ empty_project = create(:project)
294
+ expect(empty_project.tasks.completion_percentage).to eq(0)
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,118 @@
1
+ # Basic Association Examples
2
+ # Demonstrates belongs_to, has_one, has_many relationships
3
+
4
+ # ============================================
5
+ # belongs_to - Child side (has foreign key)
6
+ # ============================================
7
+
8
+ class Book < ApplicationRecord
9
+ # Required by default (Rails 5+)
10
+ belongs_to :author
11
+
12
+ # Optional - allows NULL foreign key
13
+ belongs_to :publisher, optional: true
14
+
15
+ # Custom naming
16
+ belongs_to :category,
17
+ class_name: "Genre",
18
+ foreign_key: "genre_id",
19
+ inverse_of: :books # Required with custom FK
20
+
21
+ # With counter cache on parent
22
+ belongs_to :series, counter_cache: true
23
+
24
+ # Touch parent on save
25
+ belongs_to :library, touch: true
26
+ end
27
+
28
+ # Migration for books
29
+ # create_table :books do |t|
30
+ # t.belongs_to :author, null: false, foreign_key: true
31
+ # t.belongs_to :publisher, foreign_key: true
32
+ # t.belongs_to :genre, foreign_key: { to_table: :genres }
33
+ # t.belongs_to :series, foreign_key: true
34
+ # t.belongs_to :library, foreign_key: true
35
+ # t.string :title
36
+ # t.timestamps
37
+ # end
38
+
39
+ # ============================================
40
+ # has_one - Parent side one-to-one
41
+ # ============================================
42
+
43
+ class Supplier < ApplicationRecord
44
+ has_one :account, dependent: :destroy
45
+
46
+ # Optional association
47
+ has_one :profile
48
+
49
+ # Custom naming
50
+ has_one :representative,
51
+ class_name: "Person",
52
+ foreign_key: "company_id",
53
+ inverse_of: :employer
54
+ end
55
+
56
+ # Migration - enforce true 1:1 at database level
57
+ # create_table :accounts do |t|
58
+ # t.belongs_to :supplier, null: false, index: { unique: true }, foreign_key: true
59
+ # t.decimal :balance
60
+ # t.timestamps
61
+ # end
62
+
63
+ # ============================================
64
+ # has_many - One-to-many
65
+ # ============================================
66
+
67
+ class Author < ApplicationRecord
68
+ has_many :books, dependent: :destroy
69
+
70
+ # Scoped association
71
+ has_many :published_books,
72
+ -> { where(published: true) },
73
+ class_name: "Book"
74
+
75
+ has_many :recent_books,
76
+ -> { order(created_at: :desc).limit(5) },
77
+ class_name: "Book"
78
+
79
+ # Through association
80
+ has_many :publishers, -> { distinct }, through: :books
81
+ end
82
+
83
+ # ============================================
84
+ # Usage Examples
85
+ # ============================================
86
+
87
+ # Creating with associations
88
+ author = Author.create!(name: "Jane Austen")
89
+ book = author.books.create!(title: "Pride and Prejudice")
90
+
91
+ # Building without saving
92
+ draft = author.books.build(title: "Work in Progress")
93
+ draft.save
94
+
95
+ # Adding existing record
96
+ existing_book = Book.find(123)
97
+ author.books << existing_book # Saves immediately!
98
+
99
+ # Association queries
100
+ author.books.count # COUNT query
101
+ author.books.size # Uses counter_cache if present
102
+ author.books.empty? # Boolean check
103
+ author.book_ids # Array of IDs
104
+
105
+ # Scoped queries on association
106
+ author.books.where(published: true)
107
+ author.books.order(created_at: :desc)
108
+ author.published_books.first
109
+
110
+ # has_one building
111
+ supplier = Supplier.create!(name: "ACME Corp")
112
+ supplier.create_account!(balance: 1000)
113
+ # or
114
+ supplier.build_account(balance: 500)
115
+ supplier.save
116
+
117
+ # has_one replacement
118
+ supplier.account = Account.new(balance: 2000) # Old account nullified/destroyed