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,640 @@
1
+ # Draper Anti-Patterns and Solutions
2
+
3
+ ## Anti-Pattern 1: Fat Decorator
4
+
5
+ ### Problem
6
+
7
+ Stuffing all presentation logic into a single decorator creates a maintenance nightmare:
8
+
9
+ ```ruby
10
+ # BAD: 500+ lines, 40+ methods
11
+ class UserDecorator < ApplicationDecorator
12
+ delegate_all
13
+
14
+ # Profile methods
15
+ def full_name; end
16
+ def avatar_tag; end
17
+ def profile_summary; end
18
+ def bio_excerpt; end
19
+
20
+ # Admin methods
21
+ def admin_badge; end
22
+ def permissions_list; end
23
+ def audit_log_link; end
24
+ def role_selector; end
25
+
26
+ # Social methods
27
+ def twitter_link; end
28
+ def facebook_link; end
29
+ def linkedin_badge; end
30
+
31
+ # Notification methods
32
+ def notification_count; end
33
+ def unread_badge; end
34
+ def notification_dropdown; end
35
+
36
+ # Settings methods
37
+ def preferences_form; end
38
+ def privacy_settings; end
39
+
40
+ # ... 30 more methods
41
+ end
42
+ ```
43
+
44
+ ### Why It's Bad
45
+
46
+ - **Single Responsibility Violation**: One class handles many contexts
47
+ - **Divergent Change**: Changes to admin views require editing user profile decorator
48
+ - **Hard to Test**: Tests become large and unfocused
49
+ - **Cognitive Overload**: Developers must understand entire decorator to make changes
50
+
51
+ ### Solution: Context-Specific Decorators
52
+
53
+ ```ruby
54
+ # GOOD: Base decorator with shared methods
55
+ class UserDecorator < ApplicationDecorator
56
+ delegate_all
57
+
58
+ def full_name
59
+ "#{first_name} #{last_name}"
60
+ end
61
+
62
+ def avatar_tag(size: :medium)
63
+ h.image_tag(avatar_url(size), alt: full_name, class: "avatar avatar-#{size}")
64
+ end
65
+ end
66
+
67
+ # Admin-specific presentation
68
+ class Users::AdminDecorator < UserDecorator
69
+ def admin_badge
70
+ return unless admin?
71
+ h.content_tag(:span, "Admin", class: "badge badge-danger")
72
+ end
73
+
74
+ def permissions_list
75
+ h.content_tag(:ul) do
76
+ h.safe_join(permissions.map { |p| h.content_tag(:li, p) })
77
+ end
78
+ end
79
+
80
+ def audit_log_link
81
+ h.link_to("View Audit Log", h.admin_user_audit_path(object))
82
+ end
83
+ end
84
+
85
+ # Profile page presentation
86
+ class Users::ProfileDecorator < UserDecorator
87
+ def bio_excerpt
88
+ h.truncate(bio, length: 200, separator: ' ')
89
+ end
90
+
91
+ def social_links
92
+ links = []
93
+ links << twitter_link if twitter_handle.present?
94
+ links << linkedin_link if linkedin_url.present?
95
+ h.safe_join(links, " | ")
96
+ end
97
+ end
98
+
99
+ # Notification-specific presentation
100
+ class Users::NotificationDecorator < UserDecorator
101
+ def unread_count_badge
102
+ count = unread_notifications_count
103
+ return if count.zero?
104
+
105
+ h.content_tag(:span, count, class: "badge badge-primary")
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Usage
111
+
112
+ ```ruby
113
+ # Admin panel
114
+ @user = Users::AdminDecorator.decorate(User.find(params[:id]))
115
+
116
+ # Public profile
117
+ @user = Users::ProfileDecorator.decorate(User.find(params[:id]))
118
+
119
+ # Header notification widget
120
+ @user = Users::NotificationDecorator.decorate(current_user)
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Anti-Pattern 2: N+1 Queries
126
+
127
+ ### Problem
128
+
129
+ Decorating without eager loading causes database performance issues:
130
+
131
+ ```ruby
132
+ # Controller
133
+ def index
134
+ @posts = Post.all.decorate # No eager loading!
135
+ end
136
+
137
+ # Decorator
138
+ class PostDecorator < ApplicationDecorator
139
+ delegate_all
140
+
141
+ def author_name
142
+ author.name # N+1 query for each post!
143
+ end
144
+
145
+ def comments_count
146
+ "#{comments.count} comments" # Another N+1!
147
+ end
148
+ end
149
+ ```
150
+
151
+ ### Why It's Bad
152
+
153
+ - Each `@post.author_name` triggers a separate query
154
+ - 100 posts = 100+ queries instead of 2
155
+ - Performance degrades linearly with collection size
156
+
157
+ ### Solution: Eager Load Before Decorating
158
+
159
+ ```ruby
160
+ # GOOD: Eager load associations first
161
+ class PostsController < ApplicationController
162
+ def index
163
+ @posts = Post.includes(:author, :comments).all.decorate
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### Solution: Use Counter Caches
169
+
170
+ ```ruby
171
+ # Migration
172
+ add_column :posts, :comments_count, :integer, default: 0, null: false
173
+
174
+ # Model
175
+ class Comment < ApplicationRecord
176
+ belongs_to :post, counter_cache: true
177
+ end
178
+
179
+ # Decorator - no query needed
180
+ class PostDecorator < ApplicationDecorator
181
+ def comments_count_badge
182
+ "#{comments_count} comments" # Uses counter cache
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### Solution: Batch Loading for Complex Cases
188
+
189
+ ```ruby
190
+ class PostsController < ApplicationController
191
+ def index
192
+ @posts = Post.includes(:author)
193
+ .with_attached_images # Active Storage
194
+ .with_rich_text_content # Action Text
195
+ .decorate
196
+ end
197
+ end
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Anti-Pattern 3: Decorating Too Early
203
+
204
+ ### Problem
205
+
206
+ Using decorated objects in business logic:
207
+
208
+ ```ruby
209
+ # BAD: Passing decorator to service
210
+ class PublishService
211
+ def call(decorated_post)
212
+ decorated_post.update(published_at: Time.current)
213
+ decorated_post.notify_subscribers # Is this decorator or model method?
214
+ end
215
+ end
216
+
217
+ # Controller
218
+ def publish
219
+ @post = Post.find(params[:id]).decorate
220
+ PublishService.new.call(@post) # Decorated object in service!
221
+ end
222
+ ```
223
+
224
+ ### Why It's Bad
225
+
226
+ - Services should work with models, not decorators
227
+ - Confuses presentation and business logic
228
+ - Makes testing harder (do you stub model or decorator?)
229
+ - Decorator methods might be called accidentally in business logic
230
+
231
+ ### Solution: Decorate at the Last Moment
232
+
233
+ ```ruby
234
+ # GOOD: Services receive models
235
+ class PublishService
236
+ def call(post)
237
+ post.update(published_at: Time.current)
238
+ NotificationJob.perform_later(post.id)
239
+ end
240
+ end
241
+
242
+ # Controller - decorate only before render
243
+ class PostsController < ApplicationController
244
+ def publish
245
+ @post = Post.find(params[:id])
246
+ PublishService.new.call(@post) # Model, not decorator
247
+
248
+ @post = @post.decorate # Decorate right before render
249
+ render :show
250
+ end
251
+ end
252
+ ```
253
+
254
+ ### Rule of Thumb
255
+
256
+ > "Decorate at the last moment, right before you render the view."
257
+
258
+ ---
259
+
260
+ ## Anti-Pattern 4: Business Logic in Decorators
261
+
262
+ ### Problem
263
+
264
+ Putting calculations, validations, or state changes in decorators:
265
+
266
+ ```ruby
267
+ # BAD: Business logic in decorator
268
+ class OrderDecorator < ApplicationDecorator
269
+ delegate_all
270
+
271
+ def apply_discount(code)
272
+ discount = Discount.find_by(code: code)
273
+ object.update(discount_amount: discount.amount) # State change!
274
+ end
275
+
276
+ def calculate_tax
277
+ subtotal * tax_rate # Business calculation!
278
+ end
279
+
280
+ def valid_for_shipping?
281
+ items.any? && shipping_address.present? # Validation!
282
+ end
283
+ end
284
+ ```
285
+
286
+ ### Why It's Bad
287
+
288
+ - Violates separation of concerns
289
+ - Business logic becomes scattered
290
+ - Hard to test business rules in isolation
291
+ - Decorators become coupled to domain logic
292
+
293
+ ### Solution: Keep Business Logic in Models/Services
294
+
295
+ ```ruby
296
+ # Model handles business logic
297
+ class Order < ApplicationRecord
298
+ def calculate_tax
299
+ subtotal * tax_rate
300
+ end
301
+
302
+ def valid_for_shipping?
303
+ items.any? && shipping_address.present?
304
+ end
305
+ end
306
+
307
+ # Service handles state changes
308
+ class ApplyDiscountService
309
+ def call(order, code)
310
+ discount = Discount.find_by!(code: code)
311
+ order.update!(discount_amount: discount.amount)
312
+ end
313
+ end
314
+
315
+ # GOOD: Decorator only formats for display
316
+ class OrderDecorator < ApplicationDecorator
317
+ delegate_all
318
+
319
+ def formatted_tax
320
+ h.number_to_currency(calculate_tax) # Delegates to model
321
+ end
322
+
323
+ def shipping_status_badge
324
+ css = valid_for_shipping? ? "success" : "warning"
325
+ text = valid_for_shipping? ? "Ready to Ship" : "Missing Info"
326
+ h.content_tag(:span, text, class: "badge badge-#{css}")
327
+ end
328
+ end
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Anti-Pattern 5: Circular Decoration Reference
334
+
335
+ ### Problem
336
+
337
+ Models referencing their decorators:
338
+
339
+ ```ruby
340
+ # BAD: Model knows about decorator
341
+ class Post < ApplicationRecord
342
+ def display_title
343
+ PostDecorator.new(self).formatted_title
344
+ end
345
+
346
+ def render_preview
347
+ decorator = decorate
348
+ decorator.preview_card
349
+ end
350
+ end
351
+ ```
352
+
353
+ ### Why It's Bad
354
+
355
+ - Creates circular dependency (model → decorator → model)
356
+ - Violates unidirectional data flow
357
+ - Makes models dependent on presentation layer
358
+ - Complicates testing
359
+
360
+ ### Solution: Keep Models Unaware of Decorators
361
+
362
+ ```ruby
363
+ # GOOD: Model has no decorator knowledge
364
+ class Post < ApplicationRecord
365
+ def title_with_status
366
+ "#{title} (#{status})" # Pure model method, no formatting
367
+ end
368
+ end
369
+
370
+ # View helper or decorator handles presentation
371
+ class PostDecorator < ApplicationDecorator
372
+ delegate_all
373
+
374
+ def formatted_title
375
+ h.content_tag(:h1, title_with_status, class: css_class_for_status)
376
+ end
377
+ end
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Anti-Pattern 6: Using Decorators in Background Jobs
383
+
384
+ ### Problem
385
+
386
+ Passing decorated objects to background jobs:
387
+
388
+ ```ruby
389
+ # BAD: Decorator in job
390
+ class NotificationJob < ApplicationJob
391
+ def perform(decorated_user)
392
+ UserMailer.notification(decorated_user).deliver_now
393
+ end
394
+ end
395
+
396
+ # Controller
397
+ NotificationJob.perform_later(@user.decorate)
398
+ ```
399
+
400
+ ### Why It's Bad
401
+
402
+ - Decorators can't be serialized properly
403
+ - View context not available in background
404
+ - Job might fail or behave unexpectedly
405
+
406
+ ### Solution: Pass IDs, Decorate in Mailer if Needed
407
+
408
+ ```ruby
409
+ # GOOD: Job receives ID
410
+ class NotificationJob < ApplicationJob
411
+ def perform(user_id)
412
+ user = User.find(user_id)
413
+ UserMailer.notification(user).deliver_now
414
+ end
415
+ end
416
+
417
+ # Mailer can decorate if needed
418
+ class UserMailer < ApplicationMailer
419
+ def notification(user)
420
+ @user = user.decorate
421
+ mail(to: @user.email)
422
+ end
423
+ end
424
+
425
+ # Controller
426
+ NotificationJob.perform_later(@user.id)
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Anti-Pattern 7: Inconsistent Decoration
432
+
433
+ ### Problem
434
+
435
+ Mixing decorated and undecorated objects in the same context:
436
+
437
+ ```ruby
438
+ # BAD: Inconsistent
439
+ class PostsController < ApplicationController
440
+ def index
441
+ @featured_post = Post.featured.first.decorate
442
+ @recent_posts = Post.recent.limit(5) # Not decorated!
443
+ @popular_posts = Post.popular.decorate
444
+ end
445
+ end
446
+ ```
447
+
448
+ ### Why It's Bad
449
+
450
+ - Views must handle both types
451
+ - Leads to bugs when calling decorator methods on raw models
452
+ - Confusing for developers
453
+
454
+ ### Solution: Be Consistent
455
+
456
+ ```ruby
457
+ # GOOD: All decorated
458
+ class PostsController < ApplicationController
459
+ def index
460
+ @featured_post = Post.featured.first.decorate
461
+ @recent_posts = Post.recent.limit(5).decorate
462
+ @popular_posts = Post.popular.decorate
463
+ end
464
+ end
465
+
466
+ # Or establish clear naming convention
467
+ class PostsController < ApplicationController
468
+ def index
469
+ @featured_post = Post.featured.first.decorate
470
+ @recent_post_models = Post.recent.limit(5) # Clear it's not decorated
471
+ end
472
+ end
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Anti-Pattern 8: Overriding Model Methods Incorrectly
478
+
479
+ ### Problem
480
+
481
+ Overriding model methods without maintaining compatibility:
482
+
483
+ ```ruby
484
+ # BAD: Breaks model contract
485
+ class UserDecorator < ApplicationDecorator
486
+ delegate_all
487
+
488
+ def email
489
+ # Original returns string, now returns HTML!
490
+ h.mail_to(object.email, object.email)
491
+ end
492
+ end
493
+ ```
494
+
495
+ ### Why It's Bad
496
+
497
+ - Code expecting string gets HTML
498
+ - Breaks form helpers that use `email`
499
+ - Violates Liskov Substitution Principle
500
+
501
+ ### Solution: Use Distinct Method Names
502
+
503
+ ```ruby
504
+ # GOOD: Separate presentation method
505
+ class UserDecorator < ApplicationDecorator
506
+ delegate_all
507
+
508
+ def email_link
509
+ h.mail_to(email, email)
510
+ end
511
+
512
+ def formatted_email
513
+ h.content_tag(:span, email, class: "email")
514
+ end
515
+ end
516
+ ```
517
+
518
+ ---
519
+
520
+ ## Anti-Pattern 9: Heavy Processing in Decorators
521
+
522
+ ### Problem
523
+
524
+ Performing expensive operations in decorator methods:
525
+
526
+ ```ruby
527
+ # BAD: Expensive operations
528
+ class ReportDecorator < ApplicationDecorator
529
+ delegate_all
530
+
531
+ def summary_chart
532
+ data = object.calculate_statistics # Expensive!
533
+ ChartGenerator.new(data).to_svg # More processing!
534
+ end
535
+ end
536
+ ```
537
+
538
+ ### Why It's Bad
539
+
540
+ - Called multiple times per request if accessed multiple times
541
+ - No caching by default
542
+ - View rendering becomes slow
543
+
544
+ ### Solution: Memoize or Pre-compute
545
+
546
+ ```ruby
547
+ # GOOD: Memoize expensive operations
548
+ class ReportDecorator < ApplicationDecorator
549
+ delegate_all
550
+
551
+ def summary_chart
552
+ @summary_chart ||= generate_chart
553
+ end
554
+
555
+ private
556
+
557
+ def generate_chart
558
+ ChartGenerator.new(object.statistics).to_svg
559
+ end
560
+ end
561
+
562
+ # Better: Pre-compute in model/service
563
+ class Report < ApplicationRecord
564
+ def statistics
565
+ @statistics ||= StatisticsCalculator.new(self).call
566
+ end
567
+ end
568
+ ```
569
+
570
+ ---
571
+
572
+ ## Anti-Pattern 10: Not Using ApplicationDecorator
573
+
574
+ ### Problem
575
+
576
+ Each decorator inherits directly from Draper::Decorator:
577
+
578
+ ```ruby
579
+ # BAD: No shared base
580
+ class PostDecorator < Draper::Decorator
581
+ def formatted_date
582
+ created_at.strftime("%B %d, %Y")
583
+ end
584
+ end
585
+
586
+ class CommentDecorator < Draper::Decorator
587
+ def formatted_date
588
+ created_at.strftime("%B %d, %Y") # Duplicated!
589
+ end
590
+ end
591
+ ```
592
+
593
+ ### Why It's Bad
594
+
595
+ - Code duplication across decorators
596
+ - No place for shared helper methods
597
+ - Inconsistent formatting
598
+
599
+ ### Solution: Use ApplicationDecorator
600
+
601
+ ```ruby
602
+ # GOOD: Shared base class
603
+ class ApplicationDecorator < Draper::Decorator
604
+ def formatted_date(date = created_at, format: :long)
605
+ return "N/A" if date.blank?
606
+ h.l(date, format:)
607
+ end
608
+
609
+ def formatted_currency(amount)
610
+ h.number_to_currency(amount)
611
+ end
612
+ end
613
+
614
+ class PostDecorator < ApplicationDecorator
615
+ delegate_all
616
+ # formatted_date inherited
617
+ end
618
+
619
+ class CommentDecorator < ApplicationDecorator
620
+ delegate_all
621
+ # formatted_date inherited
622
+ end
623
+ ```
624
+
625
+ ---
626
+
627
+ ## Summary Checklist
628
+
629
+ Before committing decorator code, verify:
630
+
631
+ - [ ] Decorator only contains presentation logic
632
+ - [ ] No business logic, validations, or state changes
633
+ - [ ] Controller eager loads associations before decorating
634
+ - [ ] Decoration happens right before rendering
635
+ - [ ] No circular references between model and decorator
636
+ - [ ] Method names don't override model methods with different return types
637
+ - [ ] Expensive operations are memoized
638
+ - [ ] All related decorators inherit from ApplicationDecorator
639
+ - [ ] Decorator isn't too large (consider splitting if >200 lines)
640
+ - [ ] Background jobs receive IDs, not decorated objects