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,302 @@
1
+ # Self-Referential Association Examples
2
+ # Model relates to itself for hierarchies and graphs
3
+
4
+ # ============================================
5
+ # Simple Hierarchy: Manager/Subordinates
6
+ # ============================================
7
+
8
+ class Employee < ApplicationRecord
9
+ # Employee has one manager (who is also an Employee)
10
+ belongs_to :manager,
11
+ class_name: "Employee",
12
+ optional: true,
13
+ inverse_of: :subordinates
14
+
15
+ # Employee has many subordinates (who are also Employees)
16
+ has_many :subordinates,
17
+ class_name: "Employee",
18
+ foreign_key: "manager_id",
19
+ dependent: :nullify,
20
+ inverse_of: :manager
21
+
22
+ validates :name, presence: true
23
+
24
+ # Convenience methods
25
+ def top_manager?
26
+ manager.nil?
27
+ end
28
+
29
+ def direct_reports_count
30
+ subordinates.count
31
+ end
32
+ end
33
+
34
+ # Migration
35
+ # create_table :employees do |t|
36
+ # t.string :name, null: false
37
+ # t.belongs_to :manager, foreign_key: { to_table: :employees }
38
+ # t.timestamps
39
+ # end
40
+
41
+ # Usage
42
+ ceo = Employee.create!(name: "CEO")
43
+ vp = Employee.create!(name: "VP Engineering", manager: ceo)
44
+ dev = Employee.create!(name: "Developer", manager: vp)
45
+
46
+ dev.manager # => VP Engineering
47
+ vp.subordinates # => [Developer]
48
+ ceo.subordinates # => [VP Engineering]
49
+
50
+ # ============================================
51
+ # Friendship: Many-to-Many Self-Join
52
+ # ============================================
53
+
54
+ class User < ApplicationRecord
55
+ has_many :friendships, dependent: :destroy
56
+ has_many :friends, through: :friendships
57
+
58
+ # Inverse friendships (where user is the friend)
59
+ has_many :inverse_friendships,
60
+ class_name: "Friendship",
61
+ foreign_key: "friend_id",
62
+ dependent: :destroy
63
+ has_many :inverse_friends,
64
+ through: :inverse_friendships,
65
+ source: :user
66
+
67
+ def all_friends
68
+ friends + inverse_friends
69
+ end
70
+
71
+ def friend_with?(other_user)
72
+ friends.include?(other_user) || inverse_friends.include?(other_user)
73
+ end
74
+ end
75
+
76
+ class Friendship < ApplicationRecord
77
+ belongs_to :user
78
+ belongs_to :friend, class_name: "User"
79
+
80
+ validates :user_id, uniqueness: { scope: :friend_id }
81
+ validate :not_self_friend
82
+
83
+ private
84
+
85
+ def not_self_friend
86
+ errors.add(:friend, "can't be yourself") if user_id == friend_id
87
+ end
88
+ end
89
+
90
+ # Migration
91
+ # create_table :friendships do |t|
92
+ # t.belongs_to :user, null: false, foreign_key: true
93
+ # t.belongs_to :friend, null: false, foreign_key: { to_table: :users }
94
+ # t.timestamps
95
+ # end
96
+ # add_index :friendships, [:user_id, :friend_id], unique: true
97
+
98
+ # Usage
99
+ alice = User.create!(name: "Alice")
100
+ bob = User.create!(name: "Bob")
101
+
102
+ Friendship.create!(user: alice, friend: bob)
103
+ alice.friends.include?(bob) # => true
104
+ bob.inverse_friends.include?(alice) # => true
105
+
106
+ # ============================================
107
+ # Bidirectional Friendship (Mutual)
108
+ # ============================================
109
+
110
+ class User < ApplicationRecord
111
+ has_many :friendships, dependent: :destroy
112
+ has_many :friends, through: :friendships
113
+
114
+ def befriend(other_user)
115
+ return if self == other_user
116
+ return if friend_with?(other_user)
117
+
118
+ # Create bidirectional friendship
119
+ transaction do
120
+ friendships.create!(friend: other_user)
121
+ other_user.friendships.create!(friend: self)
122
+ end
123
+ end
124
+
125
+ def unfriend(other_user)
126
+ transaction do
127
+ friendships.find_by(friend: other_user)&.destroy
128
+ other_user.friendships.find_by(friend: self)&.destroy
129
+ end
130
+ end
131
+
132
+ def friend_with?(other_user)
133
+ friends.exists?(id: other_user.id)
134
+ end
135
+ end
136
+
137
+ # ============================================
138
+ # Tree Structure: Parent/Children
139
+ # ============================================
140
+
141
+ class Category < ApplicationRecord
142
+ belongs_to :parent,
143
+ class_name: "Category",
144
+ optional: true,
145
+ inverse_of: :children,
146
+ counter_cache: :children_count
147
+
148
+ has_many :children,
149
+ class_name: "Category",
150
+ foreign_key: "parent_id",
151
+ dependent: :destroy,
152
+ inverse_of: :parent
153
+
154
+ validates :name, presence: true
155
+
156
+ scope :roots, -> { where(parent_id: nil) }
157
+ scope :leaves, -> { where(children_count: 0) }
158
+
159
+ def root?
160
+ parent_id.nil?
161
+ end
162
+
163
+ def leaf?
164
+ children.empty?
165
+ end
166
+
167
+ def ancestors
168
+ node = self
169
+ result = []
170
+ while node.parent
171
+ result << node.parent
172
+ node = node.parent
173
+ end
174
+ result.reverse
175
+ end
176
+
177
+ def descendants
178
+ children.flat_map { |c| [c] + c.descendants }
179
+ end
180
+
181
+ def depth
182
+ ancestors.size
183
+ end
184
+ end
185
+
186
+ # Migration
187
+ # create_table :categories do |t|
188
+ # t.string :name, null: false
189
+ # t.belongs_to :parent, foreign_key: { to_table: :categories }
190
+ # t.integer :children_count, default: 0, null: false
191
+ # t.timestamps
192
+ # end
193
+
194
+ # Usage
195
+ electronics = Category.create!(name: "Electronics")
196
+ phones = Category.create!(name: "Phones", parent: electronics)
197
+ smartphones = Category.create!(name: "Smartphones", parent: phones)
198
+
199
+ smartphones.ancestors # => [Electronics, Phones]
200
+ electronics.descendants # => [Phones, Smartphones]
201
+ Category.roots # => [Electronics]
202
+
203
+ # ============================================
204
+ # Adjacency List with Recursive CTE
205
+ # ============================================
206
+
207
+ class Category < ApplicationRecord
208
+ # ... same associations as above ...
209
+
210
+ # PostgreSQL recursive query for full tree
211
+ def self.tree_for(root_id)
212
+ sql = <<~SQL
213
+ WITH RECURSIVE category_tree AS (
214
+ SELECT id, name, parent_id, 0 AS depth
215
+ FROM categories
216
+ WHERE id = :root_id
217
+ UNION ALL
218
+ SELECT c.id, c.name, c.parent_id, ct.depth + 1
219
+ FROM categories c
220
+ INNER JOIN category_tree ct ON c.parent_id = ct.id
221
+ )
222
+ SELECT * FROM category_tree ORDER BY depth, name
223
+ SQL
224
+
225
+ find_by_sql([sql, { root_id: }])
226
+ end
227
+
228
+ def full_path
229
+ self.class.ancestors_for(id).pluck(:name).join(" > ")
230
+ end
231
+
232
+ def self.ancestors_for(category_id)
233
+ sql = <<~SQL
234
+ WITH RECURSIVE ancestors AS (
235
+ SELECT id, name, parent_id, 0 AS depth
236
+ FROM categories
237
+ WHERE id = :category_id
238
+ UNION ALL
239
+ SELECT c.id, c.name, c.parent_id, a.depth + 1
240
+ FROM categories c
241
+ INNER JOIN ancestors a ON c.id = a.parent_id
242
+ )
243
+ SELECT * FROM ancestors ORDER BY depth DESC
244
+ SQL
245
+
246
+ find_by_sql([sql, { category_id: }])
247
+ end
248
+ end
249
+
250
+ # ============================================
251
+ # Follower/Following Pattern
252
+ # ============================================
253
+
254
+ class User < ApplicationRecord
255
+ # Users I follow
256
+ has_many :active_follows,
257
+ class_name: "Follow",
258
+ foreign_key: "follower_id",
259
+ dependent: :destroy
260
+ has_many :following, through: :active_follows, source: :followed
261
+
262
+ # Users who follow me
263
+ has_many :passive_follows,
264
+ class_name: "Follow",
265
+ foreign_key: "followed_id",
266
+ dependent: :destroy
267
+ has_many :followers, through: :passive_follows, source: :follower
268
+
269
+ def follow(other_user)
270
+ following << other_user unless self == other_user
271
+ end
272
+
273
+ def unfollow(other_user)
274
+ active_follows.find_by(followed: other_user)&.destroy
275
+ end
276
+
277
+ def following?(other_user)
278
+ following.exists?(id: other_user.id)
279
+ end
280
+ end
281
+
282
+ class Follow < ApplicationRecord
283
+ belongs_to :follower, class_name: "User"
284
+ belongs_to :followed, class_name: "User"
285
+
286
+ validates :follower_id, uniqueness: { scope: :followed_id }
287
+ validate :not_self_follow
288
+
289
+ private
290
+
291
+ def not_self_follow
292
+ errors.add(:followed, "can't follow yourself") if follower_id == followed_id
293
+ end
294
+ end
295
+
296
+ # Migration
297
+ # create_table :follows do |t|
298
+ # t.belongs_to :follower, null: false, foreign_key: { to_table: :users }
299
+ # t.belongs_to :followed, null: false, foreign_key: { to_table: :users }
300
+ # t.timestamps
301
+ # end
302
+ # add_index :follows, [:follower_id, :followed_id], unique: true
@@ -0,0 +1,203 @@
1
+ # Through Association Examples
2
+ # Many-to-many relationships with join models
3
+
4
+ # ============================================
5
+ # has_many :through - Standard Pattern
6
+ # ============================================
7
+
8
+ # Physician <-> Appointment <-> Patient
9
+ #
10
+ # ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
11
+ # │ Physician │ │ Appointment │ │ Patient │
12
+ # ├──────────────┤ ├──────────────┤ ├──────────────┤
13
+ # │ id │◄──────│ physician_id │ │ id │
14
+ # │ name │ │ patient_id │──────►│ name │
15
+ # │ │ │ scheduled_at │ │ │
16
+ # └──────────────┘ └──────────────┘ └──────────────┘
17
+
18
+ class Physician < ApplicationRecord
19
+ has_many :appointments, dependent: :destroy
20
+ has_many :patients, through: :appointments
21
+ end
22
+
23
+ class Appointment < ApplicationRecord
24
+ belongs_to :physician
25
+ belongs_to :patient
26
+
27
+ # Join model can have attributes
28
+ validates :scheduled_at, presence: true
29
+ validate :no_double_booking
30
+
31
+ scope :upcoming, -> { where("scheduled_at > ?", Time.current) }
32
+ scope :past, -> { where("scheduled_at <= ?", Time.current) }
33
+
34
+ private
35
+
36
+ def no_double_booking
37
+ return unless scheduled_at
38
+
39
+ conflict = Appointment.where(physician:, scheduled_at:).where.not(id:)
40
+ errors.add(:scheduled_at, "physician already has appointment") if conflict.exists?
41
+ end
42
+ end
43
+
44
+ class Patient < ApplicationRecord
45
+ has_many :appointments, dependent: :destroy
46
+ has_many :physicians, through: :appointments
47
+ end
48
+
49
+ # Migrations
50
+ # create_table :appointments do |t|
51
+ # t.belongs_to :physician, null: false, foreign_key: true
52
+ # t.belongs_to :patient, null: false, foreign_key: true
53
+ # t.datetime :scheduled_at, null: false
54
+ # t.text :notes
55
+ # t.timestamps
56
+ # end
57
+ # add_index :appointments, [:physician_id, :scheduled_at], unique: true
58
+
59
+ # ============================================
60
+ # Usage Examples
61
+ # ============================================
62
+
63
+ # Creating through association
64
+ physician = Physician.find(1)
65
+ patient = Patient.find(1)
66
+
67
+ # Via join model
68
+ appointment = Appointment.create!(
69
+ physician:,
70
+ patient:,
71
+ scheduled_at: 1.day.from_now
72
+ )
73
+
74
+ # Shortcut - creates join record automatically
75
+ physician.patients << patient # Creates Appointment
76
+
77
+ # With join model attributes
78
+ physician.appointments.create!(
79
+ patient:,
80
+ scheduled_at: 2.days.from_now,
81
+ notes: "Follow-up visit"
82
+ )
83
+
84
+ # Querying through
85
+ physician.patients.where(name: "John")
86
+ physician.appointments.upcoming
87
+
88
+ # ============================================
89
+ # has_one :through
90
+ # ============================================
91
+
92
+ # Supplier -> Account -> AccountHistory
93
+
94
+ class Supplier < ApplicationRecord
95
+ has_one :account
96
+ has_one :account_history, through: :account
97
+ end
98
+
99
+ class Account < ApplicationRecord
100
+ belongs_to :supplier
101
+ has_one :account_history
102
+ end
103
+
104
+ class AccountHistory < ApplicationRecord
105
+ belongs_to :account
106
+ end
107
+
108
+ # Usage
109
+ supplier = Supplier.first
110
+ supplier.account_history # One query through Account
111
+
112
+ # ============================================
113
+ # Nested has_many :through
114
+ # ============================================
115
+
116
+ # Document -> Section -> Paragraph
117
+
118
+ class Document < ApplicationRecord
119
+ has_many :sections
120
+ has_many :paragraphs, through: :sections
121
+ end
122
+
123
+ class Section < ApplicationRecord
124
+ belongs_to :document
125
+ has_many :paragraphs
126
+ end
127
+
128
+ class Paragraph < ApplicationRecord
129
+ belongs_to :section
130
+ end
131
+
132
+ # Access all paragraphs in document
133
+ document.paragraphs
134
+
135
+ # ============================================
136
+ # Inverse Of - Critical for Nested Attributes
137
+ # ============================================
138
+
139
+ class Invoice < ApplicationRecord
140
+ has_many :line_items, inverse_of: :invoice # REQUIRED!
141
+ accepts_nested_attributes_for :line_items
142
+
143
+ validates :total, presence: true
144
+ end
145
+
146
+ class LineItem < ApplicationRecord
147
+ belongs_to :invoice
148
+ validates :invoice, presence: true # Fails without inverse_of!
149
+ end
150
+
151
+ # Without inverse_of:
152
+ # Invoice.create!(line_items_attributes: [{...}])
153
+ # => ValidationFailed: Invoice can't be blank
154
+
155
+ # With inverse_of:
156
+ # Invoice.create!(line_items_attributes: [{...}])
157
+ # => Success! Rails knows line_item.invoice is the parent
158
+
159
+ # ============================================
160
+ # Source Option - When Names Don't Match
161
+ # ============================================
162
+
163
+ class Person < ApplicationRecord
164
+ has_many :readings
165
+ has_many :articles, through: :readings, source: :post # post, not article
166
+ end
167
+
168
+ class Reading < ApplicationRecord
169
+ belongs_to :person
170
+ belongs_to :post # Not called "article"
171
+ end
172
+
173
+ # ============================================
174
+ # HABTM vs Through Comparison
175
+ # ============================================
176
+
177
+ # HABTM - Simple but limited
178
+ class Assembly < ApplicationRecord
179
+ has_and_belongs_to_many :parts
180
+ end
181
+
182
+ class Part < ApplicationRecord
183
+ has_and_belongs_to_many :assemblies
184
+ end
185
+
186
+ # has_many :through - Flexible (PREFERRED)
187
+ class Assembly < ApplicationRecord
188
+ has_many :assembly_parts
189
+ has_many :parts, through: :assembly_parts
190
+ end
191
+
192
+ class AssemblyPart < ApplicationRecord
193
+ belongs_to :assembly
194
+ belongs_to :part
195
+
196
+ # Can add attributes later!
197
+ # quantity, position, notes, etc.
198
+ end
199
+
200
+ class Part < ApplicationRecord
201
+ has_many :assembly_parts
202
+ has_many :assemblies, through: :assembly_parts
203
+ end
@@ -0,0 +1,209 @@
1
+ # ActiveRecord CRUD Operations Examples
2
+
3
+ # =============================================================================
4
+ # CREATE
5
+ # =============================================================================
6
+
7
+ # Two-step creation
8
+ user = User.new
9
+ user.name = "Alice"
10
+ user.email = "alice@example.com"
11
+ if user.save
12
+ puts "User created: #{user.id}"
13
+ else
14
+ puts "Errors: #{user.errors.full_messages}"
15
+ end
16
+
17
+ # One-step creation
18
+ user = User.create(name: "Bob", email: "bob@example.com")
19
+ if user.persisted?
20
+ puts "User created: #{user.id}"
21
+ end
22
+
23
+ # With block
24
+ user = User.create(email: "charlie@example.com") do |u|
25
+ u.name = "Charlie"
26
+ u.role = :admin
27
+ end
28
+
29
+ # Bang methods (raise on failure)
30
+ begin
31
+ user = User.create!(name: "", email: "invalid")
32
+ rescue ActiveRecord::RecordInvalid => e
33
+ puts "Validation failed: #{e.message}"
34
+ end
35
+
36
+ # Bulk insert (skips validations and callbacks)
37
+ User.insert_all([
38
+ { name: "User 1", email: "user1@example.com", created_at: Time.current, updated_at: Time.current },
39
+ { name: "User 2", email: "user2@example.com", created_at: Time.current, updated_at: Time.current }
40
+ ])
41
+
42
+ # Upsert (insert or update on conflict)
43
+ User.upsert_all(
44
+ [{ email: "alice@example.com", name: "Alice Updated" }],
45
+ unique_by: :email
46
+ )
47
+
48
+ # =============================================================================
49
+ # READ
50
+ # =============================================================================
51
+
52
+ # find - raises RecordNotFound if not found
53
+ user = User.find(1)
54
+
55
+ # find with multiple IDs
56
+ users = User.find([1, 2, 3]) # Returns array, raises if ANY not found
57
+
58
+ # find_by - returns nil if not found
59
+ user = User.find_by(email: "alice@example.com")
60
+ user = User.find_by(status: :active, role: :admin)
61
+
62
+ # find_by! - raises if not found
63
+ user = User.find_by!(email: "nonexistent@example.com")
64
+
65
+ # where - returns Relation
66
+ users = User.where(active: true)
67
+ users = User.where("created_at > ?", 1.week.ago)
68
+ users = User.where(role: [:admin, :moderator])
69
+
70
+ # Chainable queries
71
+ users = User
72
+ .where(active: true)
73
+ .where.not(role: :guest)
74
+ .order(created_at: :desc)
75
+ .limit(10)
76
+
77
+ # First/last
78
+ user = User.first
79
+ user = User.last
80
+ user = User.order(:name).first(5) # First 5 by name
81
+
82
+ # Take (no order guarantee, faster)
83
+ user = User.take
84
+ users = User.take(3)
85
+
86
+ # =============================================================================
87
+ # UPDATE
88
+ # =============================================================================
89
+
90
+ # Standard update (with validations and callbacks)
91
+ user = User.find(1)
92
+ user.update(name: "New Name")
93
+
94
+ # Update with bang (raises on failure)
95
+ user.update!(name: "New Name")
96
+
97
+ # Multiple attributes
98
+ user.update(
99
+ name: "New Name",
100
+ email: "newemail@example.com",
101
+ settings: { theme: "dark" }
102
+ )
103
+
104
+ # Update attribute (skips validations, runs callbacks)
105
+ user.update_attribute(:verified, true)
106
+
107
+ # Update column (skips validations AND callbacks)
108
+ user.update_column(:login_count, user.login_count + 1)
109
+
110
+ # Update columns (multiple)
111
+ user.update_columns(
112
+ login_count: user.login_count + 1,
113
+ last_login_at: Time.current
114
+ )
115
+
116
+ # Conditional update
117
+ user.update(status: :premium) if user.eligible_for_premium?
118
+
119
+ # Bulk update (skips validations and callbacks)
120
+ User.where(status: :trial).update_all(status: :expired)
121
+ User.where("last_login_at < ?", 1.year.ago).update_all(active: false)
122
+
123
+ # Update with SQL expression
124
+ User.update_all("login_count = login_count + 1")
125
+
126
+ # =============================================================================
127
+ # DELETE / DESTROY
128
+ # =============================================================================
129
+
130
+ # Destroy (with callbacks and dependent associations)
131
+ user = User.find(1)
132
+ user.destroy
133
+ puts user.destroyed? # => true
134
+ puts user.frozen? # => true
135
+
136
+ # Delete (skip callbacks, orphans dependent records)
137
+ user = User.find(2)
138
+ user.delete
139
+
140
+ # Bulk destroy (with callbacks)
141
+ User.where(status: :banned).destroy_all
142
+
143
+ # Bulk delete (without callbacks)
144
+ User.where(status: :banned).delete_all
145
+
146
+ # Delete by ID
147
+ User.destroy(5) # With callbacks
148
+ User.delete(5) # Without callbacks
149
+
150
+ # =============================================================================
151
+ # FIND OR CREATE
152
+ # =============================================================================
153
+
154
+ # Find or create
155
+ user = User.find_or_create_by(email: "alice@example.com")
156
+
157
+ # With additional attributes via block
158
+ user = User.find_or_create_by(email: "alice@example.com") do |u|
159
+ u.name = "Alice"
160
+ u.role = :member
161
+ end
162
+
163
+ # Find or initialize (doesn't save)
164
+ user = User.find_or_initialize_by(email: "newuser@example.com")
165
+ user.name = "New User"
166
+ user.save if user.new_record?
167
+
168
+ # Create or find (handles race conditions - requires unique constraint)
169
+ user = User.create_or_find_by(email: "alice@example.com") do |u|
170
+ u.name = "Alice"
171
+ end
172
+
173
+ # =============================================================================
174
+ # PRACTICAL PATTERNS
175
+ # =============================================================================
176
+
177
+ # Safe lookup with fallback
178
+ def find_user(id)
179
+ User.find_by(id:) || User.new(name: "Guest")
180
+ end
181
+
182
+ # Idempotent create
183
+ def ensure_default_category
184
+ Category.find_or_create_by!(name: "Uncategorized") do |c|
185
+ c.slug = "uncategorized"
186
+ c.position = 0
187
+ end
188
+ end
189
+
190
+ # Soft delete pattern
191
+ class User < ApplicationRecord
192
+ scope :active, -> { where(deleted_at: nil) }
193
+ scope :deleted, -> { where.not(deleted_at: nil) }
194
+
195
+ def soft_delete
196
+ update(deleted_at: Time.current)
197
+ end
198
+
199
+ def restore
200
+ update(deleted_at: nil)
201
+ end
202
+ end
203
+
204
+ # Increment/decrement counters
205
+ user.increment!(:login_count)
206
+ user.decrement!(:credits)
207
+
208
+ # Toggle boolean
209
+ user.toggle!(:email_notifications)