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,218 @@
1
+ # ActiveRecord Dirty Tracking Examples
2
+
3
+ # =============================================================================
4
+ # BEFORE SAVE - Pending Changes
5
+ # =============================================================================
6
+
7
+ user = User.find(1)
8
+ user.name # => "Alice"
9
+
10
+ # Make a change
11
+ user.name = "Bob"
12
+
13
+ # Check if anything changed
14
+ user.changed? # => true
15
+ user.changes # => {"name" => ["Alice", "Bob"]}
16
+
17
+ # Check specific attribute
18
+ user.name_changed? # => true
19
+ user.name_was # => "Alice" (original value)
20
+ user.name_change # => ["Alice", "Bob"]
21
+
22
+ # Check what will be saved
23
+ user.will_save_change_to_name? # => true
24
+ user.changes_to_save # => {"name" => ["Alice", "Bob"]}
25
+ user.name_in_database # => "Alice"
26
+
27
+ # Check with options
28
+ user.name_changed?(from: "Alice") # => true
29
+ user.name_changed?(to: "Bob") # => true
30
+ user.name_changed?(from: "Alice", to: "Bob") # => true
31
+
32
+ # =============================================================================
33
+ # AFTER SAVE - Previous Changes
34
+ # =============================================================================
35
+
36
+ user.save
37
+
38
+ # Now check previous changes (what just happened)
39
+ user.saved_change_to_name? # => true
40
+ user.saved_change_to_name # => ["Alice", "Bob"]
41
+ user.name_before_last_save # => "Alice"
42
+ user.name_previously_was # => "Alice"
43
+ user.previous_changes # => {"name" => ["Alice", "Bob"], "updated_at" => [...]}
44
+
45
+ # Pending changes are now empty
46
+ user.changed? # => false
47
+ user.changes # => {}
48
+
49
+ # =============================================================================
50
+ # REVERTING CHANGES
51
+ # =============================================================================
52
+
53
+ user.name = "Changed"
54
+ user.email = "changed@example.com"
55
+
56
+ # Revert single attribute
57
+ user.restore_name!
58
+ user.name # => "Bob" (restored)
59
+
60
+ # Revert all changes
61
+ user.restore_attributes
62
+ user.email # => original value
63
+
64
+ # Reload from database (clears all dirty state)
65
+ user.name = "Something"
66
+ user.reload
67
+ user.changed? # => false
68
+
69
+ # =============================================================================
70
+ # USING IN CALLBACKS
71
+ # =============================================================================
72
+
73
+ class User < ApplicationRecord
74
+ after_save :notify_if_email_changed
75
+ after_save :log_role_change
76
+ before_save :set_confirmation_token, if: :will_save_change_to_email?
77
+
78
+ private
79
+
80
+ def notify_if_email_changed
81
+ if saved_change_to_email?
82
+ old_email, new_email = saved_change_to_email
83
+ UserMailer.email_changed(self, old_email).deliver_later
84
+ end
85
+ end
86
+
87
+ def log_role_change
88
+ if saved_change_to_role?
89
+ AuditLog.create!(
90
+ user: self,
91
+ action: "role_changed",
92
+ old_value: role_before_last_save,
93
+ new_value: role
94
+ )
95
+ end
96
+ end
97
+
98
+ def set_confirmation_token
99
+ self.confirmation_token = SecureRandom.urlsafe_base64
100
+ self.confirmed_at = nil
101
+ end
102
+ end
103
+
104
+ # =============================================================================
105
+ # CONDITIONAL UPDATES
106
+ # =============================================================================
107
+
108
+ class Order < ApplicationRecord
109
+ after_save :recalculate_if_items_changed
110
+
111
+ private
112
+
113
+ def recalculate_if_items_changed
114
+ # Only recalculate if total-affecting fields changed
115
+ if saved_change_to_discount? || saved_change_to_shipping?
116
+ recalculate_total!
117
+ end
118
+ end
119
+ end
120
+
121
+ # =============================================================================
122
+ # TRACKING SPECIFIC CHANGES
123
+ # =============================================================================
124
+
125
+ class Profile < ApplicationRecord
126
+ SENSITIVE_FIELDS = %w[email phone_number ssn].freeze
127
+
128
+ after_save :audit_sensitive_changes
129
+
130
+ private
131
+
132
+ def audit_sensitive_changes
133
+ changed_sensitive = previous_changes.keys & SENSITIVE_FIELDS
134
+
135
+ changed_sensitive.each do |field|
136
+ old_val, new_val = previous_changes[field]
137
+ SecurityAudit.log(
138
+ user: self,
139
+ field:,
140
+ changed_from: mask_value(old_val),
141
+ changed_to: mask_value(new_val)
142
+ )
143
+ end
144
+ end
145
+
146
+ def mask_value(value)
147
+ return nil if value.nil?
148
+ "***#{value.to_s.last(4)}"
149
+ end
150
+ end
151
+
152
+ # =============================================================================
153
+ # CHANGED_ATTRIBUTES VS CHANGES
154
+ # =============================================================================
155
+
156
+ user.name = "New"
157
+ user.email = "new@example.com"
158
+
159
+ # changed_attribute_names_to_save - just the names
160
+ user.changed_attribute_names_to_save # => ["name", "email"]
161
+
162
+ # attributes_in_database - original values hash
163
+ user.attributes_in_database # => {"name" => "Old", "email" => "old@example.com", ...}
164
+
165
+ # =============================================================================
166
+ # ASSOCIATION CHANGES
167
+ # =============================================================================
168
+
169
+ # Foreign key changes are tracked
170
+ post = Post.find(1)
171
+ post.author_id = 5
172
+ post.author_id_changed? # => true
173
+ post.author_id_was # => previous author_id
174
+
175
+ # =============================================================================
176
+ # IN-PLACE MODIFICATION (Rails 7+)
177
+ # =============================================================================
178
+
179
+ # Rails 7+ automatically detects in-place changes
180
+ user.tags = ["ruby"]
181
+ user.tags << "rails"
182
+ user.tags_changed? # => true (automatic detection)
183
+
184
+ # Previously required:
185
+ # user.tags_will_change! # No longer needed in Rails 7+
186
+ # user.tags << "rails"
187
+
188
+ # =============================================================================
189
+ # SKIPPING DIRTY TRACKING
190
+ # =============================================================================
191
+
192
+ # These methods bypass dirty tracking entirely
193
+ user.update_column(:login_count, 5) # No callbacks, no dirty tracking
194
+ user.update_columns(login_count: 5) # No callbacks, no dirty tracking
195
+
196
+ # Check changes BEFORE using these if needed
197
+ if user.login_count_changed?
198
+ # ... do something
199
+ end
200
+ user.update_column(:login_count, user.login_count)
201
+
202
+ # =============================================================================
203
+ # PERFORMANCE CONSIDERATIONS
204
+ # =============================================================================
205
+
206
+ # Dirty tracking adds memory overhead
207
+ # For bulk operations, use update_all to bypass
208
+
209
+ # BAD - instantiates models with dirty tracking
210
+ User.where(status: :pending).each do |user|
211
+ user.update(status: :active)
212
+ end
213
+
214
+ # GOOD - direct SQL, no instantiation
215
+ User.where(status: :pending).update_all(status: :active)
216
+
217
+ # For read-only operations, use pluck
218
+ emails = User.pluck(:email) # No model instantiation
@@ -0,0 +1,377 @@
1
+ # ActiveRecord Single Table Inheritance (STI) Examples
2
+
3
+ # =============================================================================
4
+ # BASIC STI SETUP
5
+ # =============================================================================
6
+
7
+ # Migration
8
+ class CreateVehicles < ActiveRecord::Migration[7.2]
9
+ def change
10
+ create_table :vehicles do |t|
11
+ t.string :type, null: false # Required for STI
12
+ t.string :make
13
+ t.string :model
14
+ t.integer :year
15
+ t.integer :wheels
16
+ t.float :cargo_capacity
17
+ t.timestamps
18
+
19
+ t.index :type
20
+ end
21
+ end
22
+ end
23
+
24
+ # Models
25
+ class Vehicle < ApplicationRecord
26
+ validates :make, :model, presence: true
27
+
28
+ def description
29
+ "#{year} #{make} #{model}"
30
+ end
31
+ end
32
+
33
+ class Car < Vehicle
34
+ validates :wheels, inclusion: { in: [4] }
35
+
36
+ def vehicle_type
37
+ "Automobile"
38
+ end
39
+ end
40
+
41
+ class Truck < Vehicle
42
+ validates :wheels, inclusion: { in: [4, 6, 8, 10, 18] }
43
+ validates :cargo_capacity, presence: true
44
+
45
+ def vehicle_type
46
+ "Commercial Vehicle"
47
+ end
48
+ end
49
+
50
+ class Motorcycle < Vehicle
51
+ validates :wheels, inclusion: { in: [2, 3] }
52
+
53
+ def vehicle_type
54
+ "Two-wheeler"
55
+ end
56
+ end
57
+
58
+ # =============================================================================
59
+ # STI USAGE
60
+ # =============================================================================
61
+
62
+ # Creating records
63
+ car = Car.create!(make: "Toyota", model: "Camry", year: 2024, wheels: 4)
64
+ car.type # => "Car"
65
+
66
+ truck = Truck.create!(
67
+ make: "Ford",
68
+ model: "F-150",
69
+ year: 2024,
70
+ wheels: 4,
71
+ cargo_capacity: 1500
72
+ )
73
+
74
+ # Queries automatically filter by type
75
+ Car.all
76
+ # SELECT * FROM vehicles WHERE type = 'Car'
77
+
78
+ Truck.count
79
+ # SELECT COUNT(*) FROM vehicles WHERE type = 'Truck'
80
+
81
+ # Base class queries all types
82
+ Vehicle.all
83
+ # SELECT * FROM vehicles
84
+
85
+ # Type-based queries
86
+ Vehicle.where(type: "Car")
87
+ Vehicle.where(type: ["Car", "Truck"])
88
+
89
+ # =============================================================================
90
+ # STI WITH NAMESPACED MODELS
91
+ # =============================================================================
92
+
93
+ module Inventory
94
+ class Item < ApplicationRecord
95
+ self.table_name = "inventory_items"
96
+ end
97
+
98
+ class PhysicalItem < Item
99
+ # type = "Inventory::PhysicalItem" (full class name by default)
100
+ end
101
+
102
+ class DigitalItem < Item
103
+ # type = "Inventory::DigitalItem"
104
+ end
105
+ end
106
+
107
+ # To store short type names
108
+ class ApplicationRecord < ActiveRecord::Base
109
+ self.store_full_class_name = false
110
+ # Now type = "PhysicalItem" instead of "Inventory::PhysicalItem"
111
+ end
112
+
113
+ # =============================================================================
114
+ # STI WITH SHARED SCOPES
115
+ # =============================================================================
116
+
117
+ class Vehicle < ApplicationRecord
118
+ scope :recent, -> { where("created_at > ?", 1.year.ago) }
119
+ scope :by_make, ->(make) { where(make:) }
120
+ scope :vintage, -> { where("year < ?", 1990) }
121
+ end
122
+
123
+ # Scopes work on subclasses
124
+ Car.recent.by_make("Honda")
125
+ # SELECT * FROM vehicles WHERE type = 'Car' AND created_at > ... AND make = 'Honda'
126
+
127
+ # =============================================================================
128
+ # STI WITH CALLBACKS
129
+ # =============================================================================
130
+
131
+ class Vehicle < ApplicationRecord
132
+ before_save :normalize_make
133
+
134
+ private
135
+
136
+ def normalize_make
137
+ self.make = make.titleize if make.present?
138
+ end
139
+ end
140
+
141
+ class Car < Vehicle
142
+ before_create :set_default_wheels
143
+
144
+ private
145
+
146
+ def set_default_wheels
147
+ self.wheels ||= 4
148
+ end
149
+ end
150
+
151
+ class Truck < Vehicle
152
+ after_create :notify_fleet_manager
153
+
154
+ private
155
+
156
+ def notify_fleet_manager
157
+ FleetManager.new_truck_added(self)
158
+ end
159
+ end
160
+
161
+ # =============================================================================
162
+ # STI ANTI-PATTERN: SPARSE TABLES
163
+ # =============================================================================
164
+
165
+ # BAD - Too many type-specific columns
166
+ class Vehicle < ApplicationRecord
167
+ # columns: type, make, model, wheels,
168
+ # wing_span, max_altitude, # Airplane only
169
+ # displacement, fuel_type, # Motorcycle only
170
+ # towing_capacity, bed_length # Truck only
171
+ # Results in lots of NULL values
172
+ end
173
+
174
+ # GOOD - Use delegated types or separate tables
175
+ # See delegated_types.rb for alternative
176
+
177
+ # =============================================================================
178
+ # ALTERNATIVE: DELEGATED TYPES (Rails 6.1+)
179
+ # =============================================================================
180
+
181
+ # Migration
182
+ class CreateEntries < ActiveRecord::Migration[7.2]
183
+ def change
184
+ create_table :entries do |t|
185
+ t.string :entryable_type, null: false
186
+ t.bigint :entryable_id, null: false
187
+ t.string :title
188
+ t.datetime :published_at
189
+ t.timestamps
190
+
191
+ t.index [:entryable_type, :entryable_id]
192
+ end
193
+
194
+ create_table :messages do |t|
195
+ t.text :body
196
+ t.timestamps
197
+ end
198
+
199
+ create_table :comments do |t|
200
+ t.text :content
201
+ t.bigint :parent_id
202
+ t.timestamps
203
+ end
204
+ end
205
+ end
206
+
207
+ # Models
208
+ class Entry < ApplicationRecord
209
+ delegated_type :entryable, types: %w[Message Comment], dependent: :destroy
210
+ delegate :body, to: :entryable, allow_nil: true
211
+ end
212
+
213
+ class Message < ApplicationRecord
214
+ has_one :entry, as: :entryable, touch: true
215
+ validates :body, presence: true
216
+ end
217
+
218
+ class Comment < ApplicationRecord
219
+ has_one :entry, as: :entryable, touch: true
220
+ belongs_to :parent, class_name: "Comment", optional: true
221
+ validates :content, presence: true
222
+ end
223
+
224
+ # Usage
225
+ message = Message.create!(body: "Hello world")
226
+ entry = Entry.create!(title: "First Post", entryable: message)
227
+
228
+ entry.message? # => true
229
+ entry.comment? # => false
230
+ entry.entryable # => Message instance
231
+
232
+ Entry.messages # Scope for message entries
233
+ Entry.comments # Scope for comment entries
234
+
235
+ # =============================================================================
236
+ # ALTERNATIVE: SEPARATE TABLES WITH CONCERNS
237
+ # =============================================================================
238
+
239
+ module Vehicular
240
+ extend ActiveSupport::Concern
241
+
242
+ included do
243
+ validates :make, :model, :year, presence: true
244
+ scope :recent, -> { where("year >= ?", 5.years.ago.year) }
245
+ end
246
+
247
+ def description
248
+ "#{year} #{make} #{model}"
249
+ end
250
+
251
+ def age
252
+ Time.current.year - year
253
+ end
254
+ end
255
+
256
+ class Car < ApplicationRecord
257
+ include Vehicular
258
+ validates :doors, inclusion: { in: 2..5 }
259
+ end
260
+
261
+ class Motorcycle < ApplicationRecord
262
+ include Vehicular
263
+ validates :engine_cc, presence: true
264
+ end
265
+
266
+ class Boat < ApplicationRecord
267
+ include Vehicular
268
+ validates :length_feet, presence: true
269
+ end
270
+
271
+ # =============================================================================
272
+ # STI FACTORY PATTERN
273
+ # =============================================================================
274
+
275
+ class Vehicle < ApplicationRecord
276
+ def self.build_by_type(type, **attributes)
277
+ case type.to_s.downcase
278
+ when "car" then Car.new(attributes)
279
+ when "truck" then Truck.new(attributes)
280
+ when "motorcycle" then Motorcycle.new(attributes)
281
+ else raise ArgumentError, "Unknown vehicle type: #{type}"
282
+ end
283
+ end
284
+ end
285
+
286
+ # Usage
287
+ vehicle = Vehicle.build_by_type("car", make: "Honda", model: "Civic")
288
+ vehicle.class # => Car
289
+
290
+ # =============================================================================
291
+ # QUERYING ACROSS STI HIERARCHY
292
+ # =============================================================================
293
+
294
+ # All vehicles of specific types
295
+ Vehicle.where(type: [Car.name, Truck.name])
296
+
297
+ # Using subclasses
298
+ Vehicle.where(type: Vehicle.subclasses.map(&:name))
299
+
300
+ # Exclude specific type
301
+ Vehicle.where.not(type: "Motorcycle")
302
+
303
+ # Polymorphic queries (if vehicle is used polymorphically elsewhere)
304
+ class Insurance < ApplicationRecord
305
+ belongs_to :insurable, polymorphic: true
306
+ end
307
+
308
+ # Note: This saves "Vehicle" as insurable_type, not "Car"
309
+ # Be careful with STI + polymorphic associations
310
+ car = Car.find(1)
311
+ Insurance.create!(insurable: car)
312
+ # insurable_type = "Vehicle" (base class) - may cause issues
313
+
314
+ # Workaround: store full type
315
+ class Insurance < ApplicationRecord
316
+ belongs_to :insurable, polymorphic: true
317
+
318
+ before_save :store_actual_type
319
+
320
+ private
321
+
322
+ def store_actual_type
323
+ self.insurable_type = insurable.class.name
324
+ end
325
+ end
326
+
327
+ # =============================================================================
328
+ # TESTING STI MODELS
329
+ # =============================================================================
330
+
331
+ # FactoryBot
332
+ FactoryBot.define do
333
+ factory :vehicle do
334
+ make { "Generic" }
335
+ model { "Model" }
336
+ year { 2024 }
337
+
338
+ factory :car, class: "Car" do
339
+ make { "Toyota" }
340
+ model { "Camry" }
341
+ wheels { 4 }
342
+ end
343
+
344
+ factory :truck, class: "Truck" do
345
+ make { "Ford" }
346
+ model { "F-150" }
347
+ wheels { 4 }
348
+ cargo_capacity { 1500 }
349
+ end
350
+
351
+ factory :motorcycle, class: "Motorcycle" do
352
+ make { "Harley-Davidson" }
353
+ model { "Sportster" }
354
+ wheels { 2 }
355
+ end
356
+ end
357
+ end
358
+
359
+ # RSpec
360
+ RSpec.describe Car do
361
+ it "inherits from Vehicle" do
362
+ expect(Car.superclass).to eq(Vehicle)
363
+ end
364
+
365
+ it "has correct type" do
366
+ car = create(:car)
367
+ expect(car.type).to eq("Car")
368
+ end
369
+
370
+ it "queries only cars" do
371
+ create(:car)
372
+ create(:truck)
373
+
374
+ expect(Car.count).to eq(1)
375
+ expect(Vehicle.count).to eq(2)
376
+ end
377
+ end