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,274 @@
1
+ ---
2
+ name: rails-presenter
3
+ description: Creates presenter objects for view formatting using SimpleDelegator pattern with TDD. Use when extracting view logic from models, formatting data for display, creating badges/labels, or when user mentions presenters, view models, formatting, or display helpers.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Presenter Generator (TDD)
8
+
9
+ Creates presenters that wrap models for view-specific formatting with tests first.
10
+
11
+ ## Quick Start
12
+
13
+ 1. Write failing test in `test/presenters/`
14
+ 2. Run test to confirm RED
15
+ 3. Implement presenter extending `BasePresenter`
16
+ 4. Run test to confirm GREEN
17
+
18
+ ## Project Conventions
19
+
20
+ Presenters in this project:
21
+ - Extend `BasePresenter < SimpleDelegator`
22
+ - Include ActionView helpers for formatting
23
+ - Delegate model methods via SimpleDelegator
24
+ - Return HTML-safe strings for badges/formatted output
25
+ - Use I18n for all user-facing text
26
+
27
+ ## BasePresenter (Already Exists)
28
+
29
+ ```ruby
30
+ # app/presenters/base_presenter.rb
31
+ class BasePresenter < SimpleDelegator
32
+ include ActionView::Helpers::NumberHelper
33
+ include ActionView::Helpers::DateHelper
34
+ include ActionView::Helpers::UrlHelper
35
+ include ActionView::Helpers::TagHelper
36
+ include ActionView::Helpers::TextHelper
37
+
38
+ def initialize(model, view_context = nil)
39
+ super(model)
40
+ @view_context = view_context
41
+ end
42
+
43
+ def model
44
+ __getobj__
45
+ end
46
+
47
+ alias_method :object, :model
48
+ end
49
+ ```
50
+
51
+ ## TDD Workflow
52
+
53
+ ### Step 1: Create Presenter Test (RED)
54
+
55
+ ```ruby
56
+ # test/presenters/event_presenter_test.rb
57
+ require "test_helper"
58
+
59
+ class EventPresenterTest < ActiveSupport::TestCase
60
+ setup do
61
+ @event = events(:one)
62
+ @presenter = EventPresenter.new(@event)
63
+ end
64
+
65
+ test "delegates to the model" do
66
+ assert_equal @event.name, @presenter.name
67
+ end
68
+
69
+ test "responds to model methods" do
70
+ assert_respond_to @presenter, :name
71
+ assert_respond_to @presenter, :status
72
+ assert_respond_to @presenter, :created_at
73
+ end
74
+
75
+ test "exposes the underlying model" do
76
+ assert_equal @event, @presenter.model
77
+ end
78
+
79
+ test "#display_name returns the formatted name" do
80
+ assert_equal @event.name, @presenter.display_name
81
+ end
82
+
83
+ test "#formatted_date returns formatted date when present" do
84
+ @event.update(event_date: Date.new(2026, 7, 15))
85
+ result = @presenter.formatted_date
86
+ assert_includes result, "2026"
87
+ end
88
+
89
+ test "#formatted_date returns placeholder when nil" do
90
+ @event.update(event_date: nil)
91
+ result = @presenter.formatted_date
92
+ assert_includes result, "text-slate-400"
93
+ assert_includes result, "italic"
94
+ end
95
+
96
+ test "#status_badge returns HTML-safe string" do
97
+ assert_predicate @presenter.status_badge, :html_safe?
98
+ end
99
+
100
+ test "#status_badge includes status text" do
101
+ assert_includes @presenter.status_badge, @event.status.humanize
102
+ end
103
+
104
+ test "#status_badge uses correct color for active" do
105
+ @event.update(status: :active)
106
+ presenter = EventPresenter.new(@event)
107
+ assert_includes presenter.status_badge, "bg-green-100"
108
+ end
109
+
110
+ test "#status_badge uses correct color for inactive" do
111
+ @event.update(status: :inactive)
112
+ presenter = EventPresenter.new(@event)
113
+ assert_includes presenter.status_badge, "bg-red-100"
114
+ end
115
+
116
+ test "#formatted_amount formats cents as currency" do
117
+ @event.update(amount_cents: 15000)
118
+ assert_equal "150,00 EUR", @presenter.formatted_amount
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Step 2: Run Test (Confirm RED)
124
+
125
+ ```bash
126
+ bin/rails test test/presenters/event_presenter_test.rb
127
+ ```
128
+
129
+ ### Step 3: Implement Presenter (GREEN)
130
+
131
+ ```ruby
132
+ # app/presenters/event_presenter.rb
133
+ class EventPresenter < BasePresenter
134
+ STATUS_COLORS = {
135
+ active: "bg-green-100 text-green-800",
136
+ inactive: "bg-red-100 text-red-800",
137
+ pending: "bg-yellow-100 text-yellow-800"
138
+ }.freeze
139
+
140
+ DEFAULT_COLOR = "bg-slate-100 text-slate-800"
141
+
142
+ def display_name
143
+ name
144
+ end
145
+
146
+ def formatted_date
147
+ return not_specified_span if event_date.nil?
148
+ I18n.l(event_date, format: :long)
149
+ end
150
+
151
+ def status_badge
152
+ tag.span(
153
+ status_text,
154
+ class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{status_color}"
155
+ )
156
+ end
157
+
158
+ def formatted_amount
159
+ return "0,00 EUR" if amount_cents.nil? || amount_cents.zero?
160
+ number_to_currency(
161
+ amount_cents / 100.0,
162
+ unit: "EUR",
163
+ separator: ",",
164
+ delimiter: " ",
165
+ format: "%n %u"
166
+ )
167
+ end
168
+
169
+ private
170
+
171
+ def status_text
172
+ I18n.t("activerecord.attributes.event.statuses.#{status}", default: status.to_s.humanize)
173
+ end
174
+
175
+ def status_color
176
+ STATUS_COLORS.fetch(status.to_sym, DEFAULT_COLOR)
177
+ end
178
+
179
+ def not_specified_span
180
+ tag.span(
181
+ I18n.t("presenters.common.not_specified"),
182
+ class: "text-slate-400 italic"
183
+ )
184
+ end
185
+ end
186
+ ```
187
+
188
+ ### Step 4: Run Test (Confirm GREEN)
189
+
190
+ ```bash
191
+ bin/rails test test/presenters/event_presenter_test.rb
192
+ ```
193
+
194
+ ## Common Presenter Methods
195
+
196
+ ### Date Formatting
197
+
198
+ ```ruby
199
+ def formatted_event_date
200
+ return not_specified_span if event_date.nil?
201
+ I18n.l(event_date, format: :long)
202
+ end
203
+
204
+ def short_date
205
+ return "\u2014" if event_date.nil?
206
+ event_date.strftime("%d/%m/%Y")
207
+ end
208
+ ```
209
+
210
+ ### Currency Formatting
211
+
212
+ ```ruby
213
+ def formatted_budget
214
+ return not_specified_span if budget_cents.nil?
215
+ number_to_currency(
216
+ budget_cents / 100.0,
217
+ unit: "EUR",
218
+ separator: ",",
219
+ delimiter: " ",
220
+ format: "%n %u",
221
+ precision: 0
222
+ )
223
+ end
224
+ ```
225
+
226
+ ### Badge/Tag Generation
227
+
228
+ ```ruby
229
+ def type_badge
230
+ tag.span(
231
+ display_type,
232
+ class: "inline-flex items-center px-2 py-1 rounded text-xs font-medium #{type_color}"
233
+ )
234
+ end
235
+ ```
236
+
237
+ ### Contact Links
238
+
239
+ ```ruby
240
+ def display_email
241
+ return not_specified_span if email.blank?
242
+ mail_to(email, email, class: "text-blue-600 hover:underline")
243
+ end
244
+
245
+ def display_phone
246
+ return not_specified_span if phone.blank?
247
+ link_to(phone, "tel:#{phone}", class: "text-blue-600 hover:underline")
248
+ end
249
+ ```
250
+
251
+ ## Usage in Controllers
252
+
253
+ ```ruby
254
+ # Single resource
255
+ @event = EventPresenter.new(@event)
256
+
257
+ # Collection
258
+ @events = events.map { |e| EventPresenter.new(e) }
259
+
260
+ # With view context (for route helpers)
261
+ @event = EventPresenter.new(@event, view_context)
262
+ ```
263
+
264
+ ## Checklist
265
+
266
+ - [ ] Test written first (RED)
267
+ - [ ] Extends `BasePresenter`
268
+ - [ ] Delegation tested
269
+ - [ ] HTML output is `html_safe`
270
+ - [ ] Uses I18n for all text
271
+ - [ ] Currency stored in cents, displayed formatted
272
+ - [ ] Color mappings use constants (Open/Closed)
273
+ - [ ] `not_specified_span` for nil values
274
+ - [ ] All tests GREEN
@@ -0,0 +1,289 @@
1
+ ---
2
+ name: rails-query-object
3
+ description: Creates query objects for complex database queries following TDD. Use when encapsulating complex queries, aggregating statistics, building reports, or when user mentions queries, stats, dashboards, or data aggregation.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Query Object Generator (TDD)
8
+
9
+ Creates query objects that encapsulate complex database queries with tests first.
10
+
11
+ ## Quick Start
12
+
13
+ 1. Write failing test in `test/queries/`
14
+ 2. Run test to confirm RED
15
+ 3. Implement query object in `app/queries/`
16
+ 4. Run test to confirm GREEN
17
+
18
+ ## When to Use Query Objects vs Scopes
19
+
20
+ | Scenario | Use |
21
+ |----------|-----|
22
+ | Simple WHERE clause | **Scope** on the model |
23
+ | Single-condition filter | **Scope** on the model |
24
+ | Multi-table joins with conditions | **Query object** |
25
+ | Dashboard aggregations | **Query object** |
26
+ | Report generation | **Query object** |
27
+ | Queries needing constructor params | **Query object** |
28
+ | Reusable across controllers | **Query object** |
29
+
30
+ **Rule of thumb:** If the query fits in one line and needs no context, use a scope. If it needs parameters, joins multiple tables, or returns computed data, use a query object.
31
+
32
+ ## Project Conventions
33
+
34
+ Query objects in this project:
35
+ - Accept context via constructor (`user:` or `account:`)
36
+ - Return `ActiveRecord::Relation` for chainability OR `Hash` for aggregations
37
+ - Have a `call` method for primary operation
38
+ - Support multi-tenancy (scoped to account)
39
+
40
+ ## TDD Workflow
41
+
42
+ ### Step 1: Create Query Test (RED)
43
+
44
+ ```ruby
45
+ # test/queries/stale_leads_query_test.rb
46
+ require "test_helper"
47
+
48
+ class StaleLeadsQueryTest < ActiveSupport::TestCase
49
+ setup do
50
+ @account = accounts(:one)
51
+ @other_account = accounts(:two)
52
+ end
53
+
54
+ test "requires an account parameter" do
55
+ assert_raises(ArgumentError) { StaleLeadsQuery.new }
56
+ end
57
+
58
+ test "#call returns ActiveRecord::Relation" do
59
+ query = StaleLeadsQuery.new(account: @account)
60
+ assert_kind_of ActiveRecord::Relation, query.call
61
+ end
62
+
63
+ test "#call returns only leads for the account (multi-tenant)" do
64
+ own_lead = leads(:stale_one)
65
+ other_lead = leads(:other_account_stale)
66
+
67
+ results = StaleLeadsQuery.new(account: @account).call
68
+
69
+ assert_includes results, own_lead
70
+ assert_not_includes results, other_lead
71
+ end
72
+
73
+ test "#call returns only stale leads" do
74
+ stale = leads(:stale_one)
75
+ fresh = leads(:fresh_one)
76
+
77
+ results = StaleLeadsQuery.new(account: @account).call
78
+
79
+ assert_includes results, stale
80
+ assert_not_includes results, fresh
81
+ end
82
+
83
+ test "multi-tenant isolation" do
84
+ other_query = StaleLeadsQuery.new(account: @other_account)
85
+ own_query = StaleLeadsQuery.new(account: @account)
86
+
87
+ assert_empty(other_query.call.where(id: leads(:stale_one).id))
88
+ assert_not_empty(own_query.call.where(id: leads(:stale_one).id))
89
+ end
90
+ end
91
+ ```
92
+
93
+ ### Step 2: Run Test (Confirm RED)
94
+
95
+ ```bash
96
+ bin/rails test test/queries/stale_leads_query_test.rb
97
+ ```
98
+
99
+ ### Step 3: Implement Query Object (GREEN)
100
+
101
+ ```ruby
102
+ # app/queries/stale_leads_query.rb
103
+ class StaleLeadsQuery
104
+ attr_reader :account
105
+
106
+ def initialize(account:)
107
+ @account = account
108
+ end
109
+
110
+ def call
111
+ account.leads.stale
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Step 4: Run Test (Confirm GREEN)
117
+
118
+ ```bash
119
+ bin/rails test test/queries/stale_leads_query_test.rb
120
+ ```
121
+
122
+ ## Query Object Patterns
123
+
124
+ ### Pattern 1: Simple Filtered Query
125
+
126
+ ```ruby
127
+ # app/queries/stale_leads_query.rb
128
+ class StaleLeadsQuery
129
+ attr_reader :account
130
+
131
+ def initialize(account:)
132
+ @account = account
133
+ end
134
+
135
+ def call
136
+ account.leads.stale
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Pattern 2: Aggregation Query (Multiple Methods)
142
+
143
+ ```ruby
144
+ # app/queries/dashboard_stats_query.rb
145
+ class DashboardStatsQuery
146
+ attr_reader :user, :account
147
+
148
+ def initialize(user:)
149
+ @user = user
150
+ @account = user.account
151
+ end
152
+
153
+ def upcoming_events(limit: 3)
154
+ account.events
155
+ .where("event_date >= ?", Date.today)
156
+ .order(event_date: :asc)
157
+ .limit(limit)
158
+ end
159
+
160
+ def pending_commissions_total
161
+ EventVendor
162
+ .joins(:event)
163
+ .where(events: { account_id: account.id })
164
+ .where(commission_status: :to_invoice)
165
+ .sum(:commission_value)
166
+ end
167
+
168
+ def top_vendors(limit: 5)
169
+ account.vendors
170
+ .left_joins(:event_vendors)
171
+ .select("vendors.*, COUNT(event_vendors.id) as events_count")
172
+ .group("vendors.id")
173
+ .order("events_count DESC")
174
+ .limit(limit)
175
+ end
176
+
177
+ def leads_by_status
178
+ account.leads.group(:status).count
179
+ end
180
+ end
181
+ ```
182
+
183
+ ### Pattern 3: Grouping Query
184
+
185
+ ```ruby
186
+ # app/queries/leads_by_status_query.rb
187
+ class LeadsByStatusQuery
188
+ attr_reader :account
189
+
190
+ def initialize(account:)
191
+ @account = account
192
+ end
193
+
194
+ def call
195
+ leads = account.leads.order(created_at: :desc)
196
+ result = Lead.statuses.keys.map(&:to_sym).index_with { [] }
197
+
198
+ leads.group_by(&:status).each do |status, status_leads|
199
+ result[status.to_sym] = status_leads
200
+ end
201
+
202
+ result
203
+ end
204
+ end
205
+ ```
206
+
207
+ ### Testing Aggregation Queries
208
+
209
+ ```ruby
210
+ # test/queries/dashboard_stats_query_test.rb
211
+ require "test_helper"
212
+
213
+ class DashboardStatsQueryTest < ActiveSupport::TestCase
214
+ setup do
215
+ @user = users(:one)
216
+ @query = DashboardStatsQuery.new(user: @user)
217
+ end
218
+
219
+ test "#upcoming_events returns future events only" do
220
+ results = @query.upcoming_events
221
+ results.each do |event|
222
+ assert event.event_date >= Date.today
223
+ end
224
+ end
225
+
226
+ test "#upcoming_events respects limit" do
227
+ results = @query.upcoming_events(limit: 2)
228
+ assert results.size <= 2
229
+ end
230
+
231
+ test "#leads_by_status returns hash of status to count" do
232
+ result = @query.leads_by_status
233
+ assert_kind_of Hash, result
234
+ end
235
+
236
+ test "scoped to user account only" do
237
+ other_user = users(:other_account)
238
+ other_query = DashboardStatsQuery.new(user: other_user)
239
+
240
+ own_events = @query.upcoming_events
241
+ other_events = other_query.upcoming_events
242
+
243
+ own_events.each do |event|
244
+ assert_equal @user.account_id, event.account_id
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ ## Usage in Controllers
251
+
252
+ ```ruby
253
+ # Simple query
254
+ def index
255
+ @leads_by_status = LeadsByStatusQuery.new(account: current_account).call
256
+ end
257
+
258
+ # Aggregation query with presenter
259
+ def index
260
+ stats_query = DashboardStatsQuery.new(user: current_user)
261
+ @stats = DashboardStatsPresenter.new(stats_query)
262
+ end
263
+ ```
264
+
265
+ ## Directory Structure
266
+
267
+ ```
268
+ app/queries/
269
+ stale_leads_query.rb
270
+ leads_by_status_query.rb
271
+ dashboard_stats_query.rb
272
+ events/
273
+ upcoming_query.rb
274
+ by_vendor_query.rb
275
+ test/queries/
276
+ stale_leads_query_test.rb
277
+ dashboard_stats_query_test.rb
278
+ ```
279
+
280
+ ## Checklist
281
+
282
+ - [ ] Test written first (RED)
283
+ - [ ] Constructor accepts context (`user:` or `account:`)
284
+ - [ ] Multi-tenant isolation tested
285
+ - [ ] Return type documented
286
+ - [ ] Methods have clear, descriptive names
287
+ - [ ] Complex queries use `.includes()` to prevent N+1
288
+ - [ ] Database-agnostic (no PostgreSQL-specific SQL)
289
+ - [ ] All tests GREEN