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,403 @@
1
+ # ActiveRecord Migration Examples: Reversible Patterns
2
+ # Techniques for writing migrations that can be rolled back safely
3
+
4
+ # =============================================================================
5
+ # AUTO-REVERSIBLE OPERATIONS
6
+ # =============================================================================
7
+
8
+ # These work with `change` method - Rails handles reversal automatically
9
+
10
+ class AutoReversibleOperations < ActiveRecord::Migration[7.2]
11
+ def change
12
+ # Table operations
13
+ create_table :products do |t|
14
+ t.string :name
15
+ t.timestamps
16
+ end
17
+
18
+ create_join_table :products, :categories
19
+
20
+ # Column operations
21
+ add_column :users, :phone, :string
22
+ add_timestamps :legacy_table
23
+
24
+ # Reference operations
25
+ add_reference :orders, :user, foreign_key: true
26
+
27
+ # Index operations
28
+ add_index :users, :email, unique: true
29
+
30
+ # Constraint operations
31
+ add_foreign_key :orders, :users
32
+ add_check_constraint :products, "price > 0", name: "price_positive"
33
+
34
+ # Rename operations
35
+ rename_table :old_name, :new_name
36
+ rename_column :users, :name, :full_name
37
+ rename_index :users, :old_index, :new_index
38
+
39
+ # Extension operations (PostgreSQL)
40
+ enable_extension "pgcrypto"
41
+ end
42
+ end
43
+
44
+ # =============================================================================
45
+ # REVERSIBLE WITH from/to
46
+ # =============================================================================
47
+
48
+ # These operations need explicit from/to values
49
+
50
+ class ReversibleWithFromTo < ActiveRecord::Migration[7.2]
51
+ def change
52
+ # Default changes
53
+ change_column_default :users, :status, from: nil, to: "active"
54
+ change_column_default :products, :quantity, from: 0, to: 1
55
+
56
+ # Comment changes
57
+ change_column_comment :users, :email, from: nil, to: "Primary contact email"
58
+ change_table_comment :users, from: nil, to: "Application users"
59
+ end
60
+ end
61
+
62
+ # =============================================================================
63
+ # REVERSIBLE WITH COLUMN TYPE
64
+ # =============================================================================
65
+
66
+ # Remove operations need type info for recreation on rollback
67
+
68
+ class ReversibleRemoveOperations < ActiveRecord::Migration[7.2]
69
+ def change
70
+ # Single column - must include type
71
+ remove_column :users, :legacy_token, :string
72
+
73
+ # Multiple columns - must include type option
74
+ remove_columns :users, :temp1, :temp2, type: :string
75
+
76
+ # With all options for exact recreation
77
+ remove_column :products, :discount, :decimal,
78
+ precision: 5, scale: 2, default: 0.0
79
+ end
80
+ end
81
+
82
+ # =============================================================================
83
+ # REVERSIBLE DROP TABLE
84
+ # =============================================================================
85
+
86
+ # drop_table must include full schema for reversibility
87
+
88
+ class ReversibleDropTable < ActiveRecord::Migration[7.2]
89
+ def change
90
+ drop_table :legacy_reports do |t|
91
+ t.string :name, null: false
92
+ t.text :content
93
+ t.references :user, foreign_key: true
94
+ t.timestamps
95
+ end
96
+
97
+ # With table options
98
+ drop_table :archived_data, id: :uuid do |t|
99
+ t.jsonb :data
100
+ t.timestamps
101
+ end
102
+ end
103
+ end
104
+
105
+ # =============================================================================
106
+ # REVERSIBLE REMOVE INDEX
107
+ # =============================================================================
108
+
109
+ class ReversibleRemoveIndex < ActiveRecord::Migration[7.2]
110
+ def change
111
+ # By column (reversible)
112
+ remove_index :users, :email
113
+
114
+ # Composite index
115
+ remove_index :orders, [:user_id, :status]
116
+
117
+ # With all options for exact recreation
118
+ remove_index :users, :username, unique: true, name: "idx_users_username"
119
+ end
120
+ end
121
+
122
+ # =============================================================================
123
+ # REVERSIBLE REMOVE FOREIGN KEY
124
+ # =============================================================================
125
+
126
+ class ReversibleRemoveForeignKey < ActiveRecord::Migration[7.2]
127
+ def change
128
+ # By table (reversible)
129
+ remove_foreign_key :orders, :users
130
+
131
+ # By column - must include to_table
132
+ remove_foreign_key :orders, column: :buyer_id, to_table: :users
133
+
134
+ # With options
135
+ remove_foreign_key :order_items, :products,
136
+ on_delete: :cascade, on_update: :cascade
137
+ end
138
+ end
139
+
140
+ # =============================================================================
141
+ # USING reversible BLOCK
142
+ # =============================================================================
143
+
144
+ # For custom SQL or complex operations
145
+
146
+ class UsingReversibleBlock < ActiveRecord::Migration[7.2]
147
+ def change
148
+ create_table :products do |t|
149
+ t.string :name
150
+ t.decimal :price
151
+ t.timestamps
152
+ end
153
+
154
+ # Custom constraint with reversible
155
+ reversible do |dir|
156
+ dir.up do
157
+ execute <<~SQL
158
+ ALTER TABLE products
159
+ ADD CONSTRAINT price_range
160
+ CHECK (price BETWEEN 0 AND 1000000)
161
+ SQL
162
+ end
163
+
164
+ dir.down do
165
+ execute <<~SQL
166
+ ALTER TABLE products
167
+ DROP CONSTRAINT price_range
168
+ SQL
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Multiple reversible blocks
175
+ class ComplexReversibleMigration < ActiveRecord::Migration[7.2]
176
+ def change
177
+ add_column :orders, :status, :string
178
+
179
+ reversible do |dir|
180
+ dir.up do
181
+ execute "UPDATE orders SET status = 'pending' WHERE status IS NULL"
182
+ end
183
+ # No down - data would be lost anyway
184
+ end
185
+
186
+ change_column_null :orders, :status, false
187
+
188
+ reversible do |dir|
189
+ dir.up do
190
+ execute "CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status)"
191
+ end
192
+ dir.down do
193
+ execute "DROP INDEX CONCURRENTLY idx_orders_status"
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ # =============================================================================
200
+ # EXPLICIT up/down METHODS
201
+ # =============================================================================
202
+
203
+ # Use when operation is truly irreversible or complex
204
+
205
+ class ExplicitUpDown < ActiveRecord::Migration[7.2]
206
+ def up
207
+ # Change column type (irreversible without explicit down)
208
+ change_column :products, :price, :decimal, precision: 12, scale: 2
209
+
210
+ # Data transformation
211
+ execute <<~SQL
212
+ UPDATE products
213
+ SET price = price * 100
214
+ WHERE price_type = 'dollars'
215
+ SQL
216
+ end
217
+
218
+ def down
219
+ execute <<~SQL
220
+ UPDATE products
221
+ SET price = price / 100
222
+ WHERE price_type = 'dollars'
223
+ SQL
224
+
225
+ change_column :products, :price, :decimal, precision: 10, scale: 2
226
+ end
227
+ end
228
+
229
+ # =============================================================================
230
+ # IRREVERSIBLE MIGRATIONS
231
+ # =============================================================================
232
+
233
+ # Explicitly mark migrations that cannot be reversed
234
+
235
+ class IrreversibleMigration < ActiveRecord::Migration[7.2]
236
+ def up
237
+ # Data destruction - cannot be reversed
238
+ execute "DELETE FROM audit_logs WHERE created_at < '2020-01-01'"
239
+
240
+ # Remove column without saving type info
241
+ remove_column :users, :legacy_data
242
+ end
243
+
244
+ def down
245
+ raise ActiveRecord::IrreversibleMigration,
246
+ "Cannot restore deleted audit logs or legacy_data column contents"
247
+ end
248
+ end
249
+
250
+ # Partial irreversibility
251
+ class PartiallyReversible < ActiveRecord::Migration[7.2]
252
+ def up
253
+ add_column :users, :full_name, :string
254
+
255
+ # Combine first_name + last_name into full_name
256
+ execute <<~SQL
257
+ UPDATE users SET full_name = first_name || ' ' || last_name
258
+ SQL
259
+
260
+ remove_column :users, :first_name
261
+ remove_column :users, :last_name
262
+ end
263
+
264
+ def down
265
+ add_column :users, :first_name, :string
266
+ add_column :users, :last_name, :string
267
+
268
+ # Best effort - split on space (may not match original)
269
+ execute <<~SQL
270
+ UPDATE users SET
271
+ first_name = split_part(full_name, ' ', 1),
272
+ last_name = substring(full_name from position(' ' in full_name) + 1)
273
+ SQL
274
+
275
+ remove_column :users, :full_name
276
+ end
277
+ end
278
+
279
+ # =============================================================================
280
+ # CONDITIONAL REVERSIBILITY
281
+ # =============================================================================
282
+
283
+ # Different behavior in up vs down
284
+
285
+ class ConditionalMigration < ActiveRecord::Migration[7.2]
286
+ def change
287
+ # These are always reversible
288
+ add_column :users, :verified_at, :datetime
289
+ add_column :users, :verified_by_id, :bigint
290
+
291
+ # Data population only on up
292
+ reversible do |dir|
293
+ dir.up do
294
+ # Set verified_at for users who completed verification
295
+ execute <<~SQL
296
+ UPDATE users
297
+ SET verified_at = completed_at
298
+ WHERE verification_status = 'completed'
299
+ SQL
300
+ end
301
+ # No down - we'd lose when they were actually verified
302
+ end
303
+
304
+ add_index :users, :verified_at
305
+ end
306
+ end
307
+
308
+ # =============================================================================
309
+ # TESTING REVERSIBILITY
310
+ # =============================================================================
311
+
312
+ # Always test migrations in both directions
313
+
314
+ # From command line:
315
+ # rails db:migrate VERSION=20240101000000
316
+ # rails db:rollback STEP=1
317
+ # rails db:migrate
318
+
319
+ # Or use the migrate alias (recommended):
320
+ # rails db:migrate db:rollback && rails db:migrate
321
+
322
+ # In RSpec (if you have migration tests):
323
+ # describe Migration do
324
+ # it "migrates up and down" do
325
+ # migrate_up
326
+ # expect(User.column_names).to include("full_name")
327
+ #
328
+ # migrate_down
329
+ # expect(User.column_names).not_to include("full_name")
330
+ # end
331
+ # end
332
+
333
+ # =============================================================================
334
+ # PRACTICAL PATTERNS
335
+ # =============================================================================
336
+
337
+ # Reversible enum creation (PostgreSQL)
338
+ class CreateStatusEnum < ActiveRecord::Migration[7.2]
339
+ def change
340
+ reversible do |dir|
341
+ dir.up do
342
+ execute <<~SQL
343
+ CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered')
344
+ SQL
345
+ end
346
+
347
+ dir.down do
348
+ execute "DROP TYPE order_status"
349
+ end
350
+ end
351
+
352
+ add_column :orders, :status, :order_status, default: "pending"
353
+ end
354
+ end
355
+
356
+ # Reversible trigger creation
357
+ class CreateAuditTrigger < ActiveRecord::Migration[7.2]
358
+ def change
359
+ reversible do |dir|
360
+ dir.up do
361
+ execute <<~SQL
362
+ CREATE OR REPLACE FUNCTION audit_changes() RETURNS TRIGGER AS $$
363
+ BEGIN
364
+ INSERT INTO audit_logs (table_name, record_id, action, created_at)
365
+ VALUES (TG_TABLE_NAME, NEW.id, TG_OP, NOW());
366
+ RETURN NEW;
367
+ END;
368
+ $$ LANGUAGE plpgsql;
369
+
370
+ CREATE TRIGGER users_audit
371
+ AFTER INSERT OR UPDATE ON users
372
+ FOR EACH ROW EXECUTE FUNCTION audit_changes();
373
+ SQL
374
+ end
375
+
376
+ dir.down do
377
+ execute <<~SQL
378
+ DROP TRIGGER IF EXISTS users_audit ON users;
379
+ DROP FUNCTION IF EXISTS audit_changes();
380
+ SQL
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ # Reversible view creation
387
+ class CreateActiveUsersView < ActiveRecord::Migration[7.2]
388
+ def change
389
+ reversible do |dir|
390
+ dir.up do
391
+ execute <<~SQL
392
+ CREATE VIEW active_users AS
393
+ SELECT * FROM users
394
+ WHERE active = true AND deleted_at IS NULL
395
+ SQL
396
+ end
397
+
398
+ dir.down do
399
+ execute "DROP VIEW active_users"
400
+ end
401
+ end
402
+ end
403
+ end