source_monitor 0.2.1 → 0.3.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.rubocop.yml +2 -0
  62. data/.ruby-version +1 -1
  63. data/.vbw-planning/.notification-log.jsonl +192 -0
  64. data/.vbw-planning/.session-log.jsonl +871 -0
  65. data/.vbw-planning/PROJECT.md +51 -0
  66. data/.vbw-planning/REQUIREMENTS.md +50 -0
  67. data/.vbw-planning/SHIPPED.md +28 -0
  68. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  69. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  70. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  71. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  72. data/.vbw-planning/codebase/INDEX.md +86 -0
  73. data/.vbw-planning/codebase/META.md +42 -0
  74. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  75. data/.vbw-planning/codebase/STACK.md +101 -0
  76. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  77. data/.vbw-planning/codebase/TESTING.md +154 -0
  78. data/.vbw-planning/config.json +12 -0
  79. data/.vbw-planning/discovery.json +24 -0
  80. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  81. data/.vbw-planning/milestones/default/STATE.md +83 -0
  82. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  86. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  96. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  106. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  113. data/CHANGELOG.md +28 -0
  114. data/CLAUDE.md +179 -0
  115. data/Gemfile +8 -0
  116. data/Gemfile.lock +113 -100
  117. data/Rakefile +2 -0
  118. data/app/controllers/source_monitor/application_controller.rb +2 -0
  119. data/app/controllers/source_monitor/health_controller.rb +2 -0
  120. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  121. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  122. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  123. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  124. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  125. data/app/controllers/source_monitor/items_controller.rb +2 -0
  126. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  127. data/app/helpers/source_monitor/application_helper.rb +4 -112
  128. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  129. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  130. data/app/jobs/source_monitor/application_job.rb +2 -0
  131. data/app/models/source_monitor/application_record.rb +2 -0
  132. data/app/models/source_monitor/log_entry.rb +0 -2
  133. data/config/coverage_baseline.json +217 -1862
  134. data/config/routes.rb +2 -0
  135. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  136. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  137. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  138. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  139. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  140. data/lib/source_monitor/assets/bundler.rb +2 -0
  141. data/lib/source_monitor/assets.rb +2 -0
  142. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  143. data/lib/source_monitor/configuration/events.rb +60 -0
  144. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  145. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  146. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  147. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  148. data/lib/source_monitor/configuration/models.rb +36 -0
  149. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  150. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  151. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  152. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  153. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  154. data/lib/source_monitor/configuration.rb +12 -579
  155. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  156. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  157. data/lib/source_monitor/dashboard/queries.rb +2 -195
  158. data/lib/source_monitor/engine.rb +2 -0
  159. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  160. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  161. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  162. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  163. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  164. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  165. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  166. data/lib/source_monitor/items/item_creator.rb +28 -455
  167. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  168. data/lib/source_monitor/setup/cli.rb +2 -0
  169. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  170. data/lib/source_monitor/setup/detectors.rb +2 -0
  171. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  172. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  173. data/lib/source_monitor/setup/install_generator.rb +2 -0
  174. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  175. data/lib/source_monitor/setup/node_installer.rb +2 -0
  176. data/lib/source_monitor/setup/prompter.rb +2 -0
  177. data/lib/source_monitor/setup/requirements.rb +2 -0
  178. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  179. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  180. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  181. data/lib/source_monitor/setup/verification/result.rb +2 -0
  182. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  185. data/lib/source_monitor/setup/workflow.rb +2 -0
  186. data/lib/source_monitor/version.rb +3 -1
  187. data/lib/source_monitor.rb +140 -58
  188. data/lib/tasks/source_monitor_assets.rake +2 -0
  189. data/lib/tasks/source_monitor_setup.rake +2 -0
  190. data/lib/tasks/source_monitor_tasks.rake +2 -0
  191. data/source_monitor.gemspec +3 -1
  192. metadata +141 -4
@@ -0,0 +1,349 @@
1
+ ---
2
+ name: rails-service-object
3
+ description: Creates service objects following single-responsibility principle with comprehensive tests. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Service Object Pattern
8
+
9
+ ## Overview
10
+
11
+ Service objects encapsulate business logic:
12
+ - Single responsibility (one public method: `#call`)
13
+ - Easy to test in isolation
14
+ - Reusable across controllers, jobs, rake tasks
15
+ - Clear input/output contract
16
+ - Dependency injection for testability
17
+
18
+ ## When to Use Service Objects
19
+
20
+ | Scenario | Use Service Object? |
21
+ |----------|---------------------|
22
+ | Complex business logic spanning multiple models | Yes |
23
+ | Multiple model interactions in one operation | Yes |
24
+ | External API calls | Yes |
25
+ | Logic shared across controllers/jobs | Yes |
26
+ | Operations with side effects (emails, webhooks) | Yes |
27
+ | Simple CRUD operations | **No** (use model) |
28
+ | Single model validation | **No** (use model) |
29
+ | Simple query/filter | **No** (use scope or query object) |
30
+ | View formatting | **No** (use presenter) |
31
+ | Form handling with validations | **No** (use form object) |
32
+
33
+ ## When NOT to Use Service Objects
34
+
35
+ **Don't create a service object when:**
36
+ - A model callback does the job (e.g., `after_create :send_welcome_email`)
37
+ - The logic is a single ActiveRecord operation
38
+ - A concern would share the behavior more naturally
39
+ - You're wrapping a single method call (adds indirection for no benefit)
40
+ - The "service" just delegates to one model method
41
+
42
+ **Rule of thumb:** If your service object's `#call` method is under 5 lines and calls one model method, you don't need it.
43
+
44
+ ## Workflow Checklist
45
+
46
+ ```
47
+ Service Object Progress:
48
+ - [ ] Step 1: Define input/output contract
49
+ - [ ] Step 2: Create service test (RED)
50
+ - [ ] Step 3: Run test (fails - no service)
51
+ - [ ] Step 4: Create service file with empty #call
52
+ - [ ] Step 5: Run test (fails - wrong return)
53
+ - [ ] Step 6: Implement #call method
54
+ - [ ] Step 7: Run test (GREEN)
55
+ - [ ] Step 8: Add error case tests
56
+ - [ ] Step 9: Implement error handling
57
+ - [ ] Step 10: Final test run
58
+ ```
59
+
60
+ ## Step 1: Define Contract
61
+
62
+ ```markdown
63
+ ## Service: Orders::CreateService
64
+
65
+ ### Purpose
66
+ Creates a new order with inventory validation and payment processing.
67
+
68
+ ### Input
69
+ - user: User (required)
70
+ - items: Array<Hash> (required) - [{product_id:, quantity:}]
71
+ - payment_method_id: Integer (optional)
72
+
73
+ ### Output (Result object)
74
+ Success: { success?: true, data: Order }
75
+ Failure: { success?: false, error: String, code: Symbol }
76
+
77
+ ### Dependencies
78
+ - inventory_service: Checks product availability
79
+ - payment_gateway: Processes payment
80
+
81
+ ### Side Effects
82
+ - Creates Order and OrderItem records
83
+ - Decrements inventory
84
+ - Charges payment method
85
+ - Sends confirmation email (async)
86
+ ```
87
+
88
+ ## Step 2: Service Test
89
+
90
+ Location: `test/services/orders/create_service_test.rb`
91
+
92
+ ```ruby
93
+ # frozen_string_literal: true
94
+
95
+ require "test_helper"
96
+
97
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
98
+ setup do
99
+ @user = users(:one)
100
+ @product = products(:available)
101
+ @items = [{ product_id: @product.id, quantity: 2 }]
102
+ @service = Orders::CreateService.new
103
+ end
104
+
105
+ test "#call with valid inputs returns success" do
106
+ result = @service.call(user: @user, items: @items)
107
+
108
+ assert result.success?
109
+ assert_instance_of Order, result.data
110
+ assert_equal @user, result.data.user
111
+ end
112
+
113
+ test "#call with valid inputs creates an order" do
114
+ assert_difference("Order.count", 1) do
115
+ @service.call(user: @user, items: @items)
116
+ end
117
+ end
118
+
119
+ test "#call with empty items returns failure" do
120
+ result = @service.call(user: @user, items: [])
121
+
122
+ assert result.failure?
123
+ assert_equal "No items provided", result.error
124
+ end
125
+
126
+ test "#call with insufficient inventory returns failure" do
127
+ items = [{ product_id: @product.id, quantity: 999_999 }]
128
+
129
+ result = @service.call(user: @user, items: items)
130
+
131
+ assert result.failure?
132
+ end
133
+
134
+ test "#call with insufficient inventory does not create order" do
135
+ items = [{ product_id: @product.id, quantity: 999_999 }]
136
+
137
+ assert_no_difference("Order.count") do
138
+ @service.call(user: @user, items: items)
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
144
+ ## Step 3-6: Implement Service
145
+
146
+ Location: `app/services/orders/create_service.rb`
147
+
148
+ ```ruby
149
+ # frozen_string_literal: true
150
+
151
+ module Orders
152
+ class CreateService
153
+ def initialize(inventory_service: InventoryService.new,
154
+ payment_gateway: PaymentGateway.new)
155
+ @inventory_service = inventory_service
156
+ @payment_gateway = payment_gateway
157
+ end
158
+
159
+ def call(user:, items:, payment_method_id: nil)
160
+ return failure("No items provided", :empty_items) if items.empty?
161
+ return failure("Insufficient inventory", :insufficient_inventory) unless inventory_available?(items)
162
+
163
+ order = create_order(user, items)
164
+ process_payment(order, payment_method_id) if payment_method_id
165
+
166
+ success(order)
167
+ rescue ActiveRecord::RecordInvalid => e
168
+ failure(e.message, :validation_failed)
169
+ end
170
+
171
+ private
172
+
173
+ attr_reader :inventory_service, :payment_gateway
174
+
175
+ def inventory_available?(items)
176
+ items.all? do |item|
177
+ inventory_service.available?(item[:product_id], item[:quantity])
178
+ end
179
+ end
180
+
181
+ def create_order(user, items)
182
+ ActiveRecord::Base.transaction do
183
+ order = Order.create!(user: user, status: :pending)
184
+
185
+ items.each do |item|
186
+ order.order_items.create!(
187
+ product_id: item[:product_id],
188
+ quantity: item[:quantity]
189
+ )
190
+ inventory_service.decrement(item[:product_id], item[:quantity])
191
+ end
192
+
193
+ order
194
+ end
195
+ end
196
+
197
+ def process_payment(order, payment_method_id)
198
+ payment_gateway.charge(
199
+ amount: order.total,
200
+ payment_method_id: payment_method_id
201
+ )
202
+ order.update!(status: :paid)
203
+ end
204
+
205
+ def success(data)
206
+ Result.new(success: true, data: data)
207
+ end
208
+
209
+ def failure(error, code = :unknown)
210
+ Result.new(success: false, error: error, code: code)
211
+ end
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Result Object
217
+
218
+ ```ruby
219
+ # app/services/result.rb
220
+ # frozen_string_literal: true
221
+
222
+ class Result
223
+ attr_reader :data, :error, :code
224
+
225
+ def initialize(success:, data: nil, error: nil, code: nil)
226
+ @success = success
227
+ @data = data
228
+ @error = error
229
+ @code = code
230
+ end
231
+
232
+ def success?
233
+ @success
234
+ end
235
+
236
+ def failure?
237
+ !@success
238
+ end
239
+
240
+ def deconstruct_keys(keys)
241
+ { success: @success, data: @data, error: @error, code: @code }
242
+ end
243
+ end
244
+ ```
245
+
246
+ ## Testing with Mocked Dependencies
247
+
248
+ ```ruby
249
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
250
+ setup do
251
+ @inventory_service = Minitest::Mock.new
252
+ @payment_gateway = Minitest::Mock.new
253
+ @service = Orders::CreateService.new(
254
+ inventory_service: @inventory_service,
255
+ payment_gateway: @payment_gateway
256
+ )
257
+ end
258
+
259
+ test "calls inventory service to check availability" do
260
+ @inventory_service.expect(:available?, true, [Integer, Integer])
261
+ @inventory_service.expect(:decrement, true, [Integer, Integer])
262
+
263
+ @service.call(user: users(:one), items: [{ product_id: 1, quantity: 2 }])
264
+
265
+ @inventory_service.verify
266
+ end
267
+ end
268
+ ```
269
+
270
+ ## Calling Services
271
+
272
+ ### From Controllers
273
+
274
+ ```ruby
275
+ class OrdersController < ApplicationController
276
+ def create
277
+ result = Orders::CreateService.new.call(
278
+ user: current_user,
279
+ items: order_params[:items],
280
+ payment_method_id: order_params[:payment_method_id]
281
+ )
282
+
283
+ if result.success?
284
+ redirect_to result.data, notice: "Order created"
285
+ else
286
+ flash.now[:alert] = result.error
287
+ render :new, status: :unprocessable_entity
288
+ end
289
+ end
290
+ end
291
+ ```
292
+
293
+ ### From Jobs
294
+
295
+ ```ruby
296
+ class ProcessOrderJob < ApplicationJob
297
+ def perform(user_id, items)
298
+ user = User.find(user_id)
299
+ result = Orders::CreateService.new.call(user: user, items: items)
300
+
301
+ unless result.success?
302
+ Rails.logger.error("Order failed: #{result.error}")
303
+ end
304
+ end
305
+ end
306
+ ```
307
+
308
+ ## Directory Structure
309
+
310
+ ```
311
+ app/services/
312
+ result.rb
313
+ orders/
314
+ create_service.rb
315
+ cancel_service.rb
316
+ users/
317
+ register_service.rb
318
+ payments/
319
+ charge_service.rb
320
+ ```
321
+
322
+ ## Conventions
323
+
324
+ 1. **Naming**: `Namespace::VerbNounService` (e.g., `Orders::CreateService`)
325
+ 2. **Location**: `app/services/[namespace]/[name]_service.rb`
326
+ 3. **Interface**: Single public method `#call`
327
+ 4. **Return**: Always return Result object
328
+ 5. **Dependencies**: Inject via constructor
329
+ 6. **Errors**: Catch and wrap in Result, don't raise
330
+
331
+ ## Anti-Patterns to Avoid
332
+
333
+ 1. **God service**: Too many responsibilities - split it
334
+ 2. **Hidden dependencies**: Using globals instead of injection
335
+ 3. **No return contract**: Returning different types
336
+ 4. **Raising exceptions**: Use Result objects instead
337
+ 5. **Service wrapping one method**: Just call the method directly
338
+ 6. **Service with multiple public methods**: Use separate services
339
+
340
+ ## Checklist
341
+
342
+ - [ ] Contract defined (input/output/side effects)
343
+ - [ ] Test written first (RED)
344
+ - [ ] Single public method `#call`
345
+ - [ ] Returns Result object consistently
346
+ - [ ] Dependencies injected via constructor
347
+ - [ ] Error cases tested
348
+ - [ ] Transaction wraps multi-model operations
349
+ - [ ] All tests GREEN
@@ -0,0 +1,307 @@
1
+ ---
2
+ name: solid-queue-setup
3
+ description: Configures Solid Queue for background jobs in Rails 8. Use when setting up background processing, creating background jobs, configuring job queues, recurring jobs, or migrating from Sidekiq to Solid Queue.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Solid Queue Setup for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Solid Queue is Rails 8's default Active Job backend:
12
+ - Database-backed (no Redis required)
13
+ - Built-in concurrency controls
14
+ - Supports priorities and multiple queues
15
+ - Web UI available via Mission Control
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ bundle add solid_queue
21
+ bin/rails solid_queue:install
22
+ bin/rails db:migrate
23
+ ```
24
+
25
+ ### Configuration
26
+
27
+ ```yaml
28
+ # config/solid_queue.yml
29
+ default: &default
30
+ dispatchers:
31
+ - polling_interval: 1
32
+ batch_size: 500
33
+ workers:
34
+ - queues: "*"
35
+ threads: 3
36
+ processes: 1
37
+ polling_interval: 0.1
38
+
39
+ development:
40
+ <<: *default
41
+
42
+ production:
43
+ <<: *default
44
+ workers:
45
+ - queues: [critical, default]
46
+ threads: 5
47
+ processes: 2
48
+ - queues: [low]
49
+ threads: 2
50
+ processes: 1
51
+ ```
52
+
53
+ ### Set as Active Job Adapter
54
+
55
+ ```ruby
56
+ # config/application.rb
57
+ config.active_job.queue_adapter = :solid_queue
58
+ ```
59
+
60
+ ## Naming Convention
61
+
62
+ Use `_later` for async, `_now` for synchronous:
63
+
64
+ ```ruby
65
+ # Async (queued via Solid Queue) - preferred
66
+ SendWelcomeEmailJob.perform_later(user.id)
67
+
68
+ # Synchronous (runs immediately, skips queue) - use sparingly
69
+ SendWelcomeEmailJob.perform_now(user.id)
70
+ ```
71
+
72
+ ## Creating Jobs
73
+
74
+ ### Basic Job
75
+
76
+ ```ruby
77
+ # app/jobs/send_welcome_email_job.rb
78
+ class SendWelcomeEmailJob < ApplicationJob
79
+ queue_as :default
80
+
81
+ def perform(user_id)
82
+ user = User.find(user_id)
83
+ UserMailer.welcome(user).deliver_now
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Job with Retries
89
+
90
+ ```ruby
91
+ # app/jobs/process_payment_job.rb
92
+ class ProcessPaymentJob < ApplicationJob
93
+ queue_as :critical
94
+
95
+ retry_on PaymentGatewayError, wait: :polynomially_longer, attempts: 5
96
+ discard_on ActiveRecord::RecordNotFound
97
+
98
+ rescue_from(StandardError) do |exception|
99
+ ErrorNotifier.notify(exception)
100
+ raise
101
+ end
102
+
103
+ def perform(order_id)
104
+ order = Order.find(order_id)
105
+ PaymentService.new.charge(order)
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Job with Priority
111
+
112
+ ```ruby
113
+ class UrgentNotificationJob < ApplicationJob
114
+ queue_as :critical
115
+
116
+ # Lower number = higher priority (default is 0)
117
+ def priority
118
+ -10
119
+ end
120
+
121
+ def perform(notification_id)
122
+ notification = Notification.find(notification_id)
123
+ notification.deliver!
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Enqueueing Jobs
129
+
130
+ ```ruby
131
+ # Enqueue immediately
132
+ SendWelcomeEmailJob.perform_later(user.id)
133
+
134
+ # Enqueue with delay
135
+ SendReminderJob.set(wait: 1.hour).perform_later(user.id)
136
+
137
+ # Enqueue at specific time
138
+ SendReportJob.set(wait_until: Date.tomorrow.noon).perform_later
139
+
140
+ # Enqueue on specific queue
141
+ ProcessJob.set(queue: :low).perform_later(data)
142
+ ```
143
+
144
+ ## Recurring Jobs
145
+
146
+ ```yaml
147
+ # config/recurring.yml
148
+ production:
149
+ daily_report:
150
+ class: GenerateDailyReportJob
151
+ schedule: every day at 6am
152
+ queue: low
153
+
154
+ cleanup:
155
+ class: CleanupOldRecordsJob
156
+ schedule: every sunday at 2am
157
+
158
+ sync:
159
+ class: SyncExternalDataJob
160
+ schedule: every 15 minutes
161
+
162
+ session_cleanup:
163
+ class: SessionCleanupJob
164
+ schedule: every day at 3am
165
+ ```
166
+
167
+ ## Testing Jobs
168
+
169
+ ### Job Test Template
170
+
171
+ ```ruby
172
+ # test/jobs/send_welcome_email_job_test.rb
173
+ require "test_helper"
174
+
175
+ class SendWelcomeEmailJobTest < ActiveJob::TestCase
176
+ setup do
177
+ @user = users(:one)
178
+ end
179
+
180
+ test "sends welcome email" do
181
+ assert_enqueued_emails 1 do
182
+ SendWelcomeEmailJob.perform_now(@user.id)
183
+ end
184
+ end
185
+
186
+ test "enqueues on default queue" do
187
+ assert_enqueued_with(job: SendWelcomeEmailJob, queue: "default") do
188
+ SendWelcomeEmailJob.perform_later(@user.id)
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ ### Testing Enqueueing
195
+
196
+ ```ruby
197
+ # test/jobs/process_payment_job_test.rb
198
+ require "test_helper"
199
+
200
+ class ProcessPaymentJobTest < ActiveJob::TestCase
201
+ test "enqueues the job with correct arguments" do
202
+ order = orders(:one)
203
+
204
+ assert_enqueued_with(job: ProcessPaymentJob, args: [order.id]) do
205
+ ProcessPaymentJob.perform_later(order.id)
206
+ end
207
+ end
208
+
209
+ test "enqueues on critical queue" do
210
+ assert_enqueued_with(job: ProcessPaymentJob, queue: "critical") do
211
+ ProcessPaymentJob.perform_later(orders(:one).id)
212
+ end
213
+ end
214
+ end
215
+ ```
216
+
217
+ ### Testing Job Side Effects
218
+
219
+ ```ruby
220
+ # test/jobs/cleanup_old_records_job_test.rb
221
+ require "test_helper"
222
+
223
+ class CleanupOldRecordsJobTest < ActiveJob::TestCase
224
+ test "deletes old sessions" do
225
+ old_session = sessions(:old)
226
+ old_session.update!(created_at: 31.days.ago)
227
+ recent_session = sessions(:one)
228
+
229
+ CleanupOldRecordsJob.perform_now
230
+
231
+ assert_not Session.exists?(old_session.id)
232
+ assert Session.exists?(recent_session.id)
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Testing with perform_enqueued_jobs
238
+
239
+ ```ruby
240
+ # test/integration/signup_flow_test.rb
241
+ require "test_helper"
242
+
243
+ class SignupFlowTest < ActionDispatch::IntegrationTest
244
+ test "signup sends welcome email" do
245
+ perform_enqueued_jobs do
246
+ post signups_path, params: {
247
+ signup: { email: "new@example.com", name: "Test" }
248
+ }
249
+ end
250
+
251
+ assert_emails 1
252
+ end
253
+ end
254
+ ```
255
+
256
+ ## Running Solid Queue
257
+
258
+ ```bash
259
+ # Development
260
+ bin/rails solid_queue:start
261
+
262
+ # Production (Procfile)
263
+ web: bin/rails server
264
+ worker: bin/rails solid_queue:start
265
+ ```
266
+
267
+ ## Monitoring
268
+
269
+ ### Mission Control (Web UI)
270
+
271
+ ```ruby
272
+ # Gemfile
273
+ gem "mission_control-jobs"
274
+
275
+ # config/routes.rb
276
+ mount MissionControl::Jobs::Engine, at: "/jobs"
277
+ ```
278
+
279
+ ### Console Queries
280
+
281
+ ```ruby
282
+ SolidQueue::Job.where(finished_at: nil).count # Pending
283
+ SolidQueue::FailedExecution.count # Failed
284
+ SolidQueue::FailedExecution.last.retry # Retry
285
+ SolidQueue::Job.where("finished_at < ?", 1.week.ago).delete_all # Cleanup
286
+ ```
287
+
288
+ ## Migration from Sidekiq
289
+
290
+ | Sidekiq | Solid Queue |
291
+ |---------|-------------|
292
+ | `perform_async(args)` | `perform_later(args)` |
293
+ | `perform_in(5.minutes, args)` | `set(wait: 5.minutes).perform_later(args)` |
294
+ | `sidekiq_options queue: 'critical'` | `queue_as :critical` |
295
+ | `sidekiq_retry_in` | `retry_on` with `wait:` |
296
+
297
+ ## Checklist
298
+
299
+ - [ ] Solid Queue gem installed
300
+ - [ ] Migrations run
301
+ - [ ] Queue adapter configured
302
+ - [ ] Jobs use `perform_later` (not `perform_now`)
303
+ - [ ] Error handling with `retry_on` / `discard_on`
304
+ - [ ] Recurring jobs configured
305
+ - [ ] Job tests written
306
+ - [ ] Mission Control mounted (optional)
307
+ - [ ] All tests GREEN