source_monitor 0.2.0 → 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 (196) 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/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: rails-architecture
3
+ description: Guides modern Rails 8 code architecture decisions and patterns. Use when deciding where to put code, choosing between patterns (service objects vs concerns vs query objects), designing feature architecture, refactoring for better organization, or when user mentions architecture, code organization, design patterns, or layered design.
4
+ allowed-tools: Read, Glob, Grep
5
+ ---
6
+
7
+ # Modern Rails 8 Architecture Patterns
8
+
9
+ ## Project Conventions
10
+ - **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
11
+ - **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
12
+ - **Authorization:** Pundit policies (deny by default)
13
+ - **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
14
+ - **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
15
+ - **State:** State-as-records for business state (booleans only for technical flags)
16
+ - **Architecture:** Rich models first, service objects for multi-model orchestration
17
+ - **Routing:** Everything-is-CRUD (new resource over new action)
18
+ - **Quality:** RuboCop (omakase) + Brakeman
19
+
20
+ ## Architecture Decision Tree
21
+
22
+ ```
23
+ Where should this code go?
24
+
25
+ ├─ Is it data validation, associations, or simple business logic?
26
+ │ └─ → Model (rich models first!)
27
+
28
+ ├─ Is it shared behavior across models?
29
+ │ └─ → Concern
30
+
31
+ ├─ Is it business state tracking (who/when/why)?
32
+ │ └─ → State Record (see: state-records pattern)
33
+
34
+ ├─ Does it orchestrate 3+ models or call external APIs?
35
+ │ └─ → Service Object (with Result pattern)
36
+
37
+ ├─ Is it a complex database query (3+ joins, aggregations)?
38
+ │ └─ → Query Object
39
+
40
+ ├─ Is it view/display formatting?
41
+ │ └─ → Presenter (SimpleDelegator)
42
+
43
+ ├─ Is it authorization logic?
44
+ │ └─ → Pundit Policy
45
+
46
+ ├─ Is it reusable UI with logic?
47
+ │ └─ → ViewComponent
48
+
49
+ ├─ Is it async/background work?
50
+ │ └─ → Shallow Job (Solid Queue)
51
+
52
+ ├─ Is it a complex form (multi-model, wizard)?
53
+ │ └─ → Form Object
54
+
55
+ ├─ Is it a transactional email?
56
+ │ └─ → Mailer
57
+
58
+ └─ Is it HTTP request/response handling only?
59
+ └─ → Controller (keep it thin!)
60
+ ```
61
+
62
+ ## Hybrid Philosophy: Models First, Services When Needed
63
+
64
+ ### The Rule of Three
65
+ - **1 model affected** → Keep logic in the model
66
+ - **2 models affected** → Consider a concern or model method
67
+ - **3+ models affected** → Extract to a service object
68
+
69
+ ### Rich Models (Default)
70
+ Models handle validations, associations, scopes, simple derived attributes, and single-model business logic. This is where most code belongs.
71
+
72
+ ```ruby
73
+ class Order < ApplicationRecord
74
+ include Closeable # State-as-records concern
75
+
76
+ belongs_to :user
77
+ has_many :line_items, dependent: :destroy
78
+
79
+ validates :total_cents, presence: true, numericality: { greater_than: 0 }
80
+
81
+ scope :recent, -> { order(created_at: :desc) }
82
+ scope :pending, -> { where.missing(:closure) }
83
+
84
+ def add_item(product, quantity: 1)
85
+ line_items.create!(product: product, quantity: quantity, price_cents: product.price_cents)
86
+ recalculate_total!
87
+ end
88
+
89
+ private
90
+
91
+ def recalculate_total!
92
+ update!(total_cents: line_items.sum("price_cents * quantity"))
93
+ end
94
+ end
95
+ ```
96
+
97
+ ### Service Objects (When Justified)
98
+ Use only when logic spans 3+ models, calls external APIs, or orchestrates complex workflows.
99
+
100
+ ```ruby
101
+ module Orders
102
+ class CheckoutService
103
+ def call(user:, cart:, payment_method_id:)
104
+ order = nil
105
+
106
+ ActiveRecord::Base.transaction do
107
+ order = user.orders.create!(total_cents: cart.total_cents)
108
+ cart.items.each { |item| order.add_item(item.product, quantity: item.quantity) }
109
+ Inventory::ReserveService.new.call(order: order)
110
+ end
111
+
112
+ Payments::ChargeService.new.call(order: order, payment_method_id: payment_method_id)
113
+ OrderMailer.confirmation(order).deliver_later
114
+ Result.new(success: true, data: order)
115
+ rescue ActiveRecord::RecordInvalid => e
116
+ Result.new(success: false, error: e.message)
117
+ end
118
+ end
119
+ end
120
+ ```
121
+
122
+ ## Everything-is-CRUD Routing
123
+
124
+ Prefer creating a new resource over adding custom actions:
125
+
126
+ ```ruby
127
+ # GOOD: New resource for publishing
128
+ resources :posts do
129
+ resource :publication, only: [:create, :destroy]
130
+ end
131
+ # POST /posts/:post_id/publication → Publications#create
132
+ # DELETE /posts/:post_id/publication → Publications#destroy
133
+
134
+ # BAD: Custom action
135
+ resources :posts do
136
+ member do
137
+ post :publish
138
+ post :unpublish
139
+ end
140
+ end
141
+ ```
142
+
143
+ ## Layer Responsibilities
144
+
145
+ | Layer | Responsibility | Should NOT contain |
146
+ |-------|---------------|-------------------|
147
+ | **Controller** | HTTP, params, authorize, render | Business logic, queries |
148
+ | **Model** | Data, validations, relations, scopes | Display logic, HTTP |
149
+ | **Concern** | Shared model/controller behavior | Unrelated cross-cutting logic |
150
+ | **Service** | Multi-model orchestration, external APIs | HTTP, display logic |
151
+ | **Query** | Complex database queries, reports | Business logic |
152
+ | **Presenter** | View formatting, badges | Business logic, queries |
153
+ | **Policy** | Authorization rules | Business logic |
154
+ | **Component** | Reusable UI encapsulation | Business logic |
155
+ | **Job** | Async delegation (shallow!) | Business logic |
156
+
157
+ ## Project Directory Structure
158
+
159
+ ```
160
+ app/
161
+ ├── channels/ # Action Cable channels
162
+ ├── components/ # ViewComponents (UI + logic)
163
+ ├── controllers/
164
+ │ └── concerns/ # Shared controller behavior
165
+ ├── forms/ # Form objects
166
+ ├── jobs/ # Background jobs (Solid Queue)
167
+ ├── mailers/ # Action Mailer classes
168
+ ├── models/
169
+ │ └── concerns/ # Shared model behavior
170
+ ├── policies/ # Pundit authorization
171
+ ├── presenters/ # View formatting
172
+ ├── queries/ # Complex queries
173
+ ├── services/ # Business logic (use sparingly)
174
+ │ └── result.rb # Shared Result class
175
+ └── views/
176
+ └── components/ # ViewComponent templates
177
+ ```
178
+
179
+ ## When NOT to Abstract
180
+
181
+ | Situation | Keep It Simple | Don't Create |
182
+ |-----------|----------------|--------------|
183
+ | Simple CRUD (< 10 lines) | Keep in controller | Service object |
184
+ | Used only once | Inline the code | Abstraction |
185
+ | Simple query with 1-2 conditions | Model scope | Query object |
186
+ | Basic text formatting | Helper method | Presenter |
187
+ | Single model form | `form_with model:` | Form object |
188
+ | Simple partial without logic | Partial | ViewComponent |
189
+
190
+ ## When TO Abstract
191
+
192
+ | Signal | Action |
193
+ |--------|--------|
194
+ | Same code in 3+ places | Extract to concern/service |
195
+ | Controller action > 15 lines | Extract to service |
196
+ | Model > 300 lines | Extract concerns |
197
+ | Complex conditionals | Extract to policy/service |
198
+ | Query joins 3+ tables | Extract to query object |
199
+ | Form spans multiple models | Extract to form object |
200
+ | Partial has > 5 lines of logic | Use ViewComponent |
201
+
202
+ ## Result Object Pattern
203
+
204
+ All services return a consistent Result:
205
+
206
+ ```ruby
207
+ # app/services/result.rb
208
+ class Result
209
+ attr_reader :data, :error, :code
210
+
211
+ def initialize(success:, data: nil, error: nil, code: nil)
212
+ @success = success
213
+ @data = data
214
+ @error = error
215
+ @code = code
216
+ end
217
+
218
+ def success? = @success
219
+ def failure? = !@success
220
+
221
+ def self.success(data = nil) = new(success: true, data: data)
222
+ def self.failure(error, code: nil) = new(success: false, error: error, code: code)
223
+ end
224
+ ```
225
+
226
+ ## Testing Strategy by Layer
227
+
228
+ | Layer | Test Type | Location | Focus |
229
+ |-------|-----------|----------|-------|
230
+ | Model | Unit | `test/models/` | Validations, scopes, methods |
231
+ | Service | Unit | `test/services/` | Business logic, edge cases |
232
+ | Query | Unit | `test/queries/` | Query results, correctness |
233
+ | Presenter | Unit | `test/presenters/` | Formatting, HTML output |
234
+ | Controller | Integration | `test/controllers/` | HTTP flow, authorization |
235
+ | Component | Component | `test/components/` | Rendering, variants |
236
+ | Policy | Unit | `test/policies/` | Authorization rules |
237
+ | System | E2E | `test/system/` | Critical user paths |
238
+
239
+ ## Anti-Patterns to Avoid
240
+
241
+ | Anti-Pattern | Problem | Solution |
242
+ |--------------|---------|----------|
243
+ | God Model | Model > 500 lines | Extract concerns |
244
+ | Fat Controller | Logic in controllers | Move to models/services |
245
+ | Premature Service | Service for 3 lines | Keep in model |
246
+ | Callback Hell | Complex model callbacks | Use services for orchestration |
247
+ | Boolean State | `approved: true` | State-as-records |
248
+ | N+1 Queries | Unoptimized queries | Use `.includes()` |
249
+
250
+ ## References
251
+
252
+ - See [layer-interactions.md](reference/layer-interactions.md) for layer communication patterns
253
+ - See [service-patterns.md](reference/service-patterns.md) for service object patterns
254
+ - See [query-patterns.md](reference/query-patterns.md) for query object patterns
255
+ - See [error-handling.md](reference/error-handling.md) for error handling strategies
256
+ - See [testing-strategy.md](reference/testing-strategy.md) for comprehensive testing
257
+ - See [multi-tenancy.md](reference/multi-tenancy.md) for multi-tenant patterns
258
+ - See [event-tracking.md](reference/event-tracking.md) for domain event patterns
259
+ - See [state-records.md](reference/state-records.md) for state-as-records patterns
@@ -0,0 +1,333 @@
1
+ # Error Handling Strategies
2
+
3
+ ## Result Object Pattern (Preferred)
4
+
5
+ Services return Result objects instead of raising exceptions:
6
+
7
+ ```ruby
8
+ # app/services/result.rb
9
+ class Result
10
+ attr_reader :data, :error, :code
11
+
12
+ def initialize(success:, data: nil, error: nil, code: nil)
13
+ @success = success
14
+ @data = data
15
+ @error = error
16
+ @code = code
17
+ end
18
+
19
+ def success? = @success
20
+ def failure? = !@success
21
+
22
+ # Pattern matching support (Ruby 3+)
23
+ def deconstruct_keys(keys)
24
+ { success: @success, data: @data, error: @error, code: @code }
25
+ end
26
+ end
27
+ ```
28
+
29
+ ## Error Code System
30
+
31
+ ### Define Error Codes
32
+
33
+ ```ruby
34
+ module Orders
35
+ class CreateService
36
+ ERROR_CODES = {
37
+ empty_cart: :empty_cart,
38
+ out_of_stock: :out_of_stock,
39
+ payment_declined: :payment_declined,
40
+ invalid_coupon: :invalid_coupon,
41
+ validation_failed: :validation_failed
42
+ }.freeze
43
+
44
+ MESSAGES = {
45
+ empty_cart: "Your cart is empty",
46
+ out_of_stock: "One or more items are out of stock",
47
+ payment_declined: "Your payment was declined",
48
+ invalid_coupon: "The coupon code is invalid",
49
+ validation_failed: "Please check your order details"
50
+ }.freeze
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### Return Typed Errors
56
+
57
+ ```ruby
58
+ def call(params)
59
+ return error(:empty_cart) if params[:items].empty?
60
+ return error(:out_of_stock) unless inventory_available?(params[:items])
61
+
62
+ order = create_order(params)
63
+ success(order)
64
+ rescue PaymentGateway::Declined
65
+ error(:payment_declined)
66
+ rescue ActiveRecord::RecordInvalid => e
67
+ error(:validation_failed, e.message)
68
+ end
69
+
70
+ private
71
+
72
+ def error(code, details = nil)
73
+ message = self.class::MESSAGES[code]
74
+ message = "#{message}: #{details}" if details
75
+ Result.new(success: false, error: message, code: code)
76
+ end
77
+ ```
78
+
79
+ ## Controller Error Handling
80
+
81
+ ### Handle by Error Code
82
+
83
+ ```ruby
84
+ class OrdersController < ApplicationController
85
+ def create
86
+ result = Orders::CreateService.new.call(order_params)
87
+
88
+ if result.success?
89
+ redirect_to result.data, notice: t(".success")
90
+ else
91
+ handle_error(result)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def handle_error(result)
98
+ case result.code
99
+ when :empty_cart
100
+ redirect_to cart_path, alert: result.error
101
+ when :out_of_stock
102
+ flash.now[:alert] = result.error
103
+ @out_of_stock = true
104
+ render :new, status: :unprocessable_entity
105
+ when :payment_declined
106
+ redirect_to payment_path, alert: result.error
107
+ else
108
+ flash.now[:alert] = result.error
109
+ render :new, status: :unprocessable_entity
110
+ end
111
+ end
112
+ end
113
+ ```
114
+
115
+ ### Pattern Matching (Ruby 3+)
116
+
117
+ ```ruby
118
+ def create
119
+ case Orders::CreateService.new.call(order_params)
120
+ in { success: true, data: order }
121
+ redirect_to order, notice: t(".success")
122
+ in { code: :empty_cart }
123
+ redirect_to cart_path, alert: t(".empty_cart")
124
+ in { code: :payment_declined, error: message }
125
+ redirect_to payment_path, alert: message
126
+ in { error: message }
127
+ flash.now[:alert] = message
128
+ render :new, status: :unprocessable_entity
129
+ end
130
+ end
131
+ ```
132
+
133
+ ## API Error Responses
134
+
135
+ ### Consistent Error Format
136
+
137
+ ```ruby
138
+ # app/controllers/api/base_controller.rb
139
+ module Api
140
+ class BaseController < ApplicationController
141
+ private
142
+
143
+ def render_error(result, status: :unprocessable_entity)
144
+ render json: {
145
+ error: {
146
+ code: result.code,
147
+ message: result.error,
148
+ details: result.data # Optional additional context
149
+ }
150
+ }, status: status
151
+ end
152
+
153
+ def render_success(data, status: :ok)
154
+ render json: { data: data }, status: status
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ ### HTTP Status Mapping
161
+
162
+ ```ruby
163
+ ERROR_STATUS_MAP = {
164
+ not_found: :not_found,
165
+ unauthorized: :unauthorized,
166
+ forbidden: :forbidden,
167
+ validation_failed: :unprocessable_entity,
168
+ conflict: :conflict,
169
+ rate_limited: :too_many_requests
170
+ }.freeze
171
+
172
+ def render_service_result(result)
173
+ if result.success?
174
+ render_success(result.data)
175
+ else
176
+ status = ERROR_STATUS_MAP.fetch(result.code, :unprocessable_entity)
177
+ render_error(result, status: status)
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Exception Handling Layers
183
+
184
+ ### Service Layer (Catch and Wrap)
185
+
186
+ ```ruby
187
+ class ExternalApiService
188
+ def call(params)
189
+ response = client.request(params)
190
+ success(response.data)
191
+ rescue Faraday::TimeoutError
192
+ error(:timeout, "External service timed out")
193
+ rescue Faraday::ConnectionFailed
194
+ error(:connection_failed, "Could not connect to service")
195
+ rescue JSON::ParserError
196
+ error(:invalid_response, "Invalid response from service")
197
+ end
198
+ end
199
+ ```
200
+
201
+ ### Controller Layer (Rescue From)
202
+
203
+ ```ruby
204
+ class ApplicationController < ActionController::Base
205
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
206
+ rescue_from Pundit::NotAuthorizedError, with: :forbidden
207
+
208
+ private
209
+
210
+ def not_found
211
+ respond_to do |format|
212
+ format.html { render "errors/not_found", status: :not_found }
213
+ format.json { render json: { error: "Not found" }, status: :not_found }
214
+ end
215
+ end
216
+
217
+ def forbidden
218
+ respond_to do |format|
219
+ format.html { redirect_to root_path, alert: t("errors.forbidden") }
220
+ format.json { render json: { error: "Forbidden" }, status: :forbidden }
221
+ end
222
+ end
223
+ end
224
+ ```
225
+
226
+ ### Global Error Handler
227
+
228
+ ```ruby
229
+ # config/initializers/error_handler.rb
230
+ Rails.application.config.exceptions_app = ->(env) {
231
+ ErrorsController.action(:show).call(env)
232
+ }
233
+
234
+ # app/controllers/errors_controller.rb
235
+ class ErrorsController < ApplicationController
236
+ skip_before_action :authenticate_user!
237
+
238
+ def show
239
+ @status = request.env["PATH_INFO"].delete("/").to_i
240
+ render status: @status
241
+ end
242
+ end
243
+ ```
244
+
245
+ ## Validation Errors
246
+
247
+ ### Model Validations to Result
248
+
249
+ ```ruby
250
+ def call(params)
251
+ record = Model.new(params)
252
+
253
+ if record.save
254
+ success(record)
255
+ else
256
+ validation_error(record)
257
+ end
258
+ end
259
+
260
+ def validation_error(record)
261
+ Result.new(
262
+ success: false,
263
+ error: record.errors.full_messages.join(", "),
264
+ code: :validation_failed,
265
+ data: record.errors.to_hash
266
+ )
267
+ end
268
+ ```
269
+
270
+ ### Display Validation Errors
271
+
272
+ ```ruby
273
+ # In controller
274
+ if result.failure? && result.code == :validation_failed
275
+ @errors = result.data # Hash of field => [messages]
276
+ end
277
+
278
+ # In view
279
+ <% if @errors&.dig(:email) %>
280
+ <p class="text-red-500"><%= @errors[:email].join(", ") %></p>
281
+ <% end %>
282
+ ```
283
+
284
+ ## Logging Errors
285
+
286
+ ```ruby
287
+ class ApplicationService
288
+ private
289
+
290
+ def error(code, message = nil, exception: nil)
291
+ log_error(code, message, exception)
292
+ Result.new(success: false, error: message || default_message(code), code: code)
293
+ end
294
+
295
+ def log_error(code, message, exception)
296
+ Rails.logger.error({
297
+ service: self.class.name,
298
+ error_code: code,
299
+ message: message,
300
+ exception: exception&.class&.name,
301
+ backtrace: exception&.backtrace&.first(5)
302
+ }.to_json)
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Error Tracking Integration
308
+
309
+ ```ruby
310
+ # With Sentry/Rollbar
311
+ def error(code, message = nil, exception: nil)
312
+ if exception && should_report?(code)
313
+ Sentry.capture_exception(exception, extra: { code: code, message: message })
314
+ end
315
+
316
+ Result.new(success: false, error: message, code: code)
317
+ end
318
+
319
+ def should_report?(code)
320
+ # Don't report expected errors
321
+ ![:validation_failed, :not_found, :unauthorized].include?(code)
322
+ end
323
+ ```
324
+
325
+ ## Checklist
326
+
327
+ - [ ] Services return Result objects
328
+ - [ ] Error codes are typed symbols
329
+ - [ ] Controllers handle errors by code
330
+ - [ ] API responses have consistent format
331
+ - [ ] Unexpected errors logged with context
332
+ - [ ] Sensitive data not exposed in errors
333
+ - [ ] User-facing messages use I18n