source_monitor 0.2.1 → 0.3.1

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 (228) 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/sm-architecture/SKILL.md +233 -0
  59. data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
  60. data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
  61. data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
  62. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
  63. data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
  64. data/.claude/skills/sm-configure/SKILL.md +153 -0
  65. data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
  66. data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
  67. data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
  68. data/.claude/skills/sm-domain-model/SKILL.md +188 -0
  69. data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
  70. data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
  71. data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
  72. data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
  73. data/.claude/skills/sm-engine-test/SKILL.md +302 -0
  74. data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
  75. data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
  76. data/.claude/skills/sm-event-handler/SKILL.md +265 -0
  77. data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
  78. data/.claude/skills/sm-health-rule/SKILL.md +327 -0
  79. data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
  80. data/.claude/skills/sm-host-setup/SKILL.md +223 -0
  81. data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
  82. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
  83. data/.claude/skills/sm-job/SKILL.md +263 -0
  84. data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
  85. data/.claude/skills/sm-model-extension/SKILL.md +287 -0
  86. data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
  87. data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
  88. data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
  89. data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
  90. data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
  91. data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
  92. data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
  93. data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
  94. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  95. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  96. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  97. data/.rubocop.yml +2 -0
  98. data/.ruby-version +1 -1
  99. data/.vbw-planning/.notification-log.jsonl +246 -0
  100. data/.vbw-planning/.session-log.jsonl +992 -0
  101. data/.vbw-planning/PROJECT.md +51 -0
  102. data/.vbw-planning/REQUIREMENTS.md +50 -0
  103. data/.vbw-planning/SHIPPED.md +28 -0
  104. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  105. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  106. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  107. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  108. data/.vbw-planning/codebase/INDEX.md +86 -0
  109. data/.vbw-planning/codebase/META.md +42 -0
  110. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  111. data/.vbw-planning/codebase/STACK.md +101 -0
  112. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  113. data/.vbw-planning/codebase/TESTING.md +154 -0
  114. data/.vbw-planning/config.json +12 -0
  115. data/.vbw-planning/discovery.json +24 -0
  116. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  117. data/.vbw-planning/milestones/default/STATE.md +83 -0
  118. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  119. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  120. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  121. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  122. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  123. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  124. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  125. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  126. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  127. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  128. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  129. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  130. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  131. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  132. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  133. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  134. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  135. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  136. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  137. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  138. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  139. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  140. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  141. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  142. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  143. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  144. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  145. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  146. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  147. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  148. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  149. data/CHANGELOG.md +37 -0
  150. data/CLAUDE.md +222 -0
  151. data/Gemfile +8 -0
  152. data/Gemfile.lock +132 -120
  153. data/Rakefile +2 -0
  154. data/app/controllers/source_monitor/application_controller.rb +2 -0
  155. data/app/controllers/source_monitor/health_controller.rb +2 -0
  156. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  157. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  158. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  159. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  160. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  161. data/app/controllers/source_monitor/items_controller.rb +2 -0
  162. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  163. data/app/helpers/source_monitor/application_helper.rb +4 -112
  164. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  165. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  166. data/app/jobs/source_monitor/application_job.rb +2 -0
  167. data/app/models/source_monitor/application_record.rb +2 -0
  168. data/app/models/source_monitor/log_entry.rb +0 -2
  169. data/config/coverage_baseline.json +217 -1862
  170. data/config/routes.rb +2 -0
  171. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  172. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  173. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  174. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  175. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  176. data/lib/source_monitor/assets/bundler.rb +2 -0
  177. data/lib/source_monitor/assets.rb +2 -0
  178. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  179. data/lib/source_monitor/configuration/events.rb +60 -0
  180. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  181. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  182. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  183. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  184. data/lib/source_monitor/configuration/models.rb +36 -0
  185. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  186. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  187. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  188. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  189. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  190. data/lib/source_monitor/configuration.rb +12 -579
  191. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  192. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  193. data/lib/source_monitor/dashboard/queries.rb +2 -195
  194. data/lib/source_monitor/engine.rb +2 -0
  195. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  196. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  197. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  198. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  199. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  200. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  201. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  202. data/lib/source_monitor/items/item_creator.rb +28 -455
  203. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  204. data/lib/source_monitor/setup/cli.rb +2 -0
  205. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  206. data/lib/source_monitor/setup/detectors.rb +2 -0
  207. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  208. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  209. data/lib/source_monitor/setup/install_generator.rb +2 -0
  210. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  211. data/lib/source_monitor/setup/node_installer.rb +2 -0
  212. data/lib/source_monitor/setup/prompter.rb +2 -0
  213. data/lib/source_monitor/setup/requirements.rb +2 -0
  214. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  215. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  216. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  217. data/lib/source_monitor/setup/verification/result.rb +2 -0
  218. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  219. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  220. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  221. data/lib/source_monitor/setup/workflow.rb +19 -2
  222. data/lib/source_monitor/version.rb +3 -1
  223. data/lib/source_monitor.rb +140 -58
  224. data/lib/tasks/source_monitor_assets.rake +2 -0
  225. data/lib/tasks/source_monitor_setup.rake +60 -0
  226. data/lib/tasks/source_monitor_tasks.rake +2 -0
  227. data/source_monitor.gemspec +4 -1
  228. metadata +177 -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,233 @@
1
+ ---
2
+ name: sm-architecture
3
+ description: Provides SourceMonitor engine architecture context. Use when working with engine internals, lib/ module structure, autoload organization, configuration DSL, pipelines, or any structural/organizational code in the source_monitor namespace.
4
+ allowed-tools: Read, Glob, Grep
5
+ user-invocable: false
6
+ ---
7
+
8
+ # SourceMonitor Architecture
9
+
10
+ ## Overview
11
+
12
+ SourceMonitor is a Rails 8 mountable engine (`SourceMonitor::Engine`). Code is split between:
13
+ - **`app/`** -- Rails conventions (models, controllers, views, jobs, concerns)
14
+ - **`lib/source_monitor/`** -- Domain logic, configuration, pipelines, utilities
15
+
16
+ The engine uses **Ruby autoload** (not Zeitwerk) for `lib/` modules, with explicit `require` only for critical boot-time modules.
17
+
18
+ ## Boot Sequence
19
+
20
+ `lib/source_monitor.rb` loads in this order:
21
+
22
+ 1. **Optional gems** (rescue LoadError): `solid_queue`, `solid_cable`, `turbo-rails`, `ransack`
23
+ 2. **Table name prefix** setup via `redefine_method`
24
+ 3. **Explicit requires** (11 files -- must load at boot):
25
+ - `version`, `engine`, `configuration`, `model_extensions`
26
+ - `events`, `instrumentation`, `metrics`
27
+ - `health`, `realtime`, `feedjira_extensions`
28
+ 4. **Autoload declarations** (71 modules) organized by namespace
29
+
30
+ ## Engine Configuration
31
+
32
+ `SourceMonitor::Engine` (`lib/source_monitor/engine.rb`):
33
+ - `isolate_namespace SourceMonitor`
34
+ - Table name prefix from `config.models.table_name_prefix`
35
+ - Initializers: assets, metrics subscribers, dashboard streams, jobs/Solid Queue setup
36
+
37
+ ## Module Tree
38
+
39
+ ```
40
+ SourceMonitor (top-level)
41
+ |-- HTTP # Faraday HTTP client factory
42
+ |-- Scheduler # Fetch scheduling coordinator
43
+ |-- Assets # Asset path helpers
44
+ |
45
+ |-- Analytics/ # Dashboard analytics queries
46
+ | |-- SourceFetchIntervalDistribution
47
+ | |-- SourceActivityRates
48
+ | |-- SourcesIndexMetrics
49
+ |
50
+ |-- Dashboard/ # Dashboard UI support
51
+ | |-- QuickAction, QuickActionsPresenter
52
+ | |-- RecentActivity, RecentActivityPresenter
53
+ | |-- Queries, TurboBroadcaster
54
+ | |-- UpcomingFetchSchedule
55
+ |
56
+ |-- Fetching/ # Feed fetch pipeline
57
+ | |-- FeedFetcher # Main orchestrator
58
+ | | |-- AdaptiveInterval # Interval calculation
59
+ | | |-- SourceUpdater # Source state updates
60
+ | | |-- EntryProcessor # Entry iteration + item creation
61
+ | |-- FetchRunner # Job-level fetch coordinator
62
+ | |-- RetryPolicy # Retry/circuit-breaker decisions
63
+ | |-- StalledFetchReconciler
64
+ | |-- AdvisoryLock # PG advisory locks
65
+ | |-- FetchError (+ subclasses)
66
+ |
67
+ |-- Items/ # Item management
68
+ | |-- ItemCreator # Create/update items from entries
69
+ | | |-- EntryParser # Parse feed entries to attributes
70
+ | | |-- ContentExtractor # Process content through readability
71
+ | |-- RetentionPruner # Age/count-based item cleanup
72
+ | |-- RetentionStrategies/ # Destroy vs SoftDelete
73
+ |
74
+ |-- ImportSessions/ # OPML import support
75
+ | |-- EntryNormalizer
76
+ | |-- HealthCheckBroadcaster
77
+ |
78
+ |-- Jobs/ # Job infrastructure
79
+ | |-- CleanupOptions
80
+ | |-- Visibility # Queue visibility setup
81
+ | |-- SolidQueueMetrics
82
+ | |-- FetchFailureSubscriber
83
+ |
84
+ |-- Logs/ # Unified log system
85
+ | |-- EntrySync # Sync log records to LogEntry
86
+ | |-- FilterSet, Query, TablePresenter
87
+ |
88
+ |-- Models/ # Model concerns
89
+ | |-- Sanitizable # String/hash sanitization
90
+ | |-- UrlNormalizable # URL normalization
91
+ |
92
+ |-- Scrapers/ # Content scraping adapters
93
+ | |-- Base # Scraper interface
94
+ | |-- Readability # Default readability adapter
95
+ | |-- Fetchers/HttpFetcher
96
+ | |-- Parsers/ReadabilityParser
97
+ |
98
+ |-- Scraping/ # Scraping orchestration
99
+ | |-- Enqueuer, Scheduler
100
+ | |-- ItemScraper (+ AdapterResolver, Persistence)
101
+ | |-- BulkSourceScraper, BulkResultPresenter
102
+ | |-- State
103
+ |
104
+ |-- Configuration/ # Configuration DSL (12 settings files)
105
+ | |-- HTTPSettings, FetchingSettings, HealthSettings
106
+ | |-- ScrapingSettings, RealtimeSettings, RetentionSettings
107
+ | |-- AuthenticationSettings, ScraperRegistry
108
+ | |-- Events, ValidationDefinition
109
+ | |-- ModelDefinition, Models
110
+ |
111
+ |-- Security/ # Security layer
112
+ | |-- ParameterSanitizer
113
+ | |-- Authentication
114
+ |
115
+ |-- Setup/ # Install/setup wizard
116
+ | |-- CLI, Workflow, Requirements
117
+ | |-- Detectors, DependencyChecker
118
+ | |-- GemfileEditor, BundleInstaller, NodeInstaller
119
+ | |-- InstallGenerator, MigrationInstaller, InitializerPatcher
120
+ | |-- Verification/ (Result, Runner, Printer, etc.)
121
+ |
122
+ |-- Pagination/Paginator
123
+ |-- Release/ (Changelog, Runner)
124
+ |-- Sources/ (Params, TurboStreamPresenter)
125
+ |-- TurboStreams/StreamResponder
126
+ ```
127
+
128
+ ## Key Architectural Patterns
129
+
130
+ ### 1. Configuration DSL
131
+
132
+ The `Configuration` class composes 12 settings objects:
133
+
134
+ ```ruby
135
+ SourceMonitor.configure do |config|
136
+ config.http.timeout = 30
137
+ config.fetching.min_interval_minutes = 5
138
+ config.health.auto_pause_threshold = 0.3
139
+ config.retention.strategy = :soft_delete
140
+ config.scraping.concurrency = 3
141
+ config.models.table_name_prefix = "sm_"
142
+ end
143
+ ```
144
+
145
+ Each settings class is a standalone PORO with defaults. Configuration is resettable via `reset_configuration!`.
146
+
147
+ ### 2. ModelExtensions Registry
148
+
149
+ Models register themselves at class load time:
150
+ ```ruby
151
+ SourceMonitor::ModelExtensions.register(self, :source)
152
+ ```
153
+
154
+ This enables:
155
+ - Dynamic table name prefixing
156
+ - Host-app concern injection
157
+ - Host-app validation injection
158
+ - Full reload on config change
159
+
160
+ ### 3. Event System
161
+
162
+ Three lifecycle events dispatched through `SourceMonitor::Events`:
163
+
164
+ | Event | Fired When | Payload |
165
+ |-------|-----------|---------|
166
+ | `after_item_created` | New item saved | ItemCreatedEvent |
167
+ | `after_item_scraped` | Scrape completed | ItemScrapedEvent |
168
+ | `after_fetch_completed` | Fetch finished | FetchCompletedEvent |
169
+
170
+ Plus `item_processors` -- callbacks run for every item (created or updated).
171
+
172
+ Events are registered via `config.events` and dispatched with error isolation per handler.
173
+
174
+ ### 4. Instrumentation (ActiveSupport::Notifications)
175
+
176
+ | Event | Purpose |
177
+ |-------|---------|
178
+ | `source_monitor.fetch.start` | Fetch beginning |
179
+ | `source_monitor.fetch.finish` | Fetch completed |
180
+ | `source_monitor.items.duplicate` | Duplicate item detected |
181
+ | `source_monitor.items.retention` | Retention pruning |
182
+
183
+ `Metrics` module subscribes to these and maintains counters/gauges.
184
+
185
+ ### 5. Pipeline Architecture
186
+
187
+ **Fetch Pipeline:**
188
+ ```
189
+ FetchRunner
190
+ -> AdvisoryLock (PG lock per source)
191
+ -> FeedFetcher.call
192
+ -> HTTP request (Faraday)
193
+ -> Parse feed (Feedjira)
194
+ -> EntryProcessor.process_feed_entries
195
+ -> ItemCreator.call (per entry)
196
+ -> EntryParser.parse
197
+ -> ContentExtractor.process_feed_content
198
+ -> Events.run_item_processors
199
+ -> Events.after_item_created
200
+ -> SourceUpdater.update_source_for_success
201
+ -> AdaptiveInterval.apply_adaptive_interval!
202
+ -> SourceUpdater.create_fetch_log
203
+ -> Events.after_fetch_completed
204
+ -> Completion handlers (retention, follow-up scraping)
205
+ ```
206
+
207
+ **Scrape Pipeline:**
208
+ ```
209
+ Scraping::Enqueuer
210
+ -> ItemScraper
211
+ -> AdapterResolver (select scraper)
212
+ -> Scrapers::Base subclass
213
+ -> Fetchers::HttpFetcher
214
+ -> Parsers::ReadabilityParser
215
+ -> Persistence (save to ItemContent)
216
+ -> Events.after_item_scraped
217
+ ```
218
+
219
+ ### 6. Health Monitoring
220
+
221
+ Health module hooks into `after_fetch_completed`:
222
+ - `SourceHealthMonitor` -- calculates rolling success rate
223
+ - `SourceHealthCheck` -- HTTP health probe
224
+ - Auto-pause sources below threshold
225
+ - `SourceHealthReset` -- manual health reset
226
+
227
+ ## References
228
+
229
+ - [Module Map](reference/module-map.md) -- Full module tree with responsibilities
230
+ - [Extraction Patterns](reference/extraction-patterns.md) -- Refactoring patterns from Phase 3/4
231
+ - Main entry: `lib/source_monitor.rb`
232
+ - Engine: `lib/source_monitor/engine.rb`
233
+ - Configuration: `lib/source_monitor/configuration.rb` + `lib/source_monitor/configuration/*.rb`