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,359 @@
1
+ ---
2
+ name: tdd-cycle
3
+ description: Guides Test-Driven Development workflow with Red-Green-Refactor cycle using Minitest and fixtures. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
4
+ allowed-tools: Read, Write, Edit, Bash
5
+ ---
6
+
7
+ # TDD Cycle — Minitest + Fixtures
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
+ ## The Cycle
21
+
22
+ ```
23
+ 1. RED → Write a failing test that describes desired behavior
24
+ 2. GREEN → Write the minimum code to pass the test
25
+ 3. REFACTOR → Improve code while keeping tests green
26
+ 4. REPEAT → Next behavior
27
+ ```
28
+
29
+ ## Workflow Checklist
30
+
31
+ ```
32
+ TDD Progress:
33
+ - [ ] Step 1: Understand the requirement
34
+ - [ ] Step 2: Choose test type (model/controller/system/component)
35
+ - [ ] Step 3: Write failing test (RED)
36
+ - [ ] Step 4: Verify test fails correctly
37
+ - [ ] Step 5: Implement minimal code (GREEN)
38
+ - [ ] Step 6: Verify test passes
39
+ - [ ] Step 7: Refactor if needed
40
+ - [ ] Step 8: Verify tests still pass
41
+ ```
42
+
43
+ ## Step 1: Requirement Analysis
44
+
45
+ Before writing any code, understand:
46
+ - What is the expected input?
47
+ - What is the expected output/behavior?
48
+ - What are the edge cases?
49
+ - What errors should be handled?
50
+
51
+ ## Step 2: Choose Test Type
52
+
53
+ | Test Type | Use For | Location |
54
+ |-----------|---------|----------|
55
+ | Model test | Validations, scopes, instance methods | `test/models/` |
56
+ | Controller test | HTTP flow, authorization, responses | `test/controllers/` |
57
+ | System test | Full user flows with JavaScript | `test/system/` |
58
+ | Service test | Business logic, complex operations | `test/services/` |
59
+ | Query test | Complex queries, correctness | `test/queries/` |
60
+ | Component test | ViewComponent rendering | `test/components/` |
61
+ | Policy test | Pundit authorization rules | `test/policies/` |
62
+ | Job test | Background job behavior | `test/jobs/` |
63
+ | Mailer test | Email content, recipients | `test/mailers/` |
64
+
65
+ ## Step 3: Write Failing Test (RED)
66
+
67
+ ### Model Test Template
68
+
69
+ ```ruby
70
+ # test/models/post_test.rb
71
+ require "test_helper"
72
+
73
+ class PostTest < ActiveSupport::TestCase
74
+ setup do
75
+ @post = posts(:published)
76
+ end
77
+
78
+ test "requires title" do
79
+ @post.title = nil
80
+ assert_not @post.valid?
81
+ assert_includes @post.errors[:title], "can't be blank"
82
+ end
83
+
84
+ test ".recent returns posts in descending order" do
85
+ recent = posts(:recent)
86
+ old = posts(:old)
87
+ assert_equal [recent, old], Post.recent.to_a
88
+ end
89
+
90
+ test "#publish! creates a publication record" do
91
+ post = posts(:draft)
92
+ assert_difference "Publication.count", 1 do
93
+ post.publish!(user: users(:admin))
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### Controller (Integration) Test Template
100
+
101
+ ```ruby
102
+ # test/controllers/posts_controller_test.rb
103
+ require "test_helper"
104
+
105
+ class PostsControllerTest < ActionDispatch::IntegrationTest
106
+ setup do
107
+ @user = users(:one)
108
+ @post = posts(:one)
109
+ sign_in_as @user
110
+ end
111
+
112
+ test "should get index" do
113
+ get posts_url
114
+ assert_response :success
115
+ end
116
+
117
+ test "should create post" do
118
+ assert_difference("Post.count") do
119
+ post posts_url, params: { post: { title: "New Post", body: "Content" } }
120
+ end
121
+ assert_redirected_to post_url(Post.last)
122
+ end
123
+
124
+ test "should not create post with invalid params" do
125
+ assert_no_difference("Post.count") do
126
+ post posts_url, params: { post: { title: "", body: "" } }
127
+ end
128
+ assert_response :unprocessable_entity
129
+ end
130
+
131
+ test "requires authentication" do
132
+ sign_out
133
+ get posts_url
134
+ assert_redirected_to new_session_url
135
+ end
136
+ end
137
+ ```
138
+
139
+ ### Service Test Template
140
+
141
+ ```ruby
142
+ # test/services/orders/create_service_test.rb
143
+ require "test_helper"
144
+
145
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
146
+ setup do
147
+ @user = users(:one)
148
+ @product = products(:widget)
149
+ @service = Orders::CreateService.new
150
+ end
151
+
152
+ test "creates order with valid params" do
153
+ result = @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
154
+
155
+ assert result.success?
156
+ assert_kind_of Order, result.data
157
+ assert_equal @user, result.data.user
158
+ end
159
+
160
+ test "returns failure with empty items" do
161
+ result = @service.call(user: @user, items: [])
162
+
163
+ assert result.failure?
164
+ assert_equal :empty_cart, result.code
165
+ end
166
+
167
+ test "wraps in transaction" do
168
+ assert_no_difference "Order.count" do
169
+ @service.call(user: @user, items: [{ product_id: 0, quantity: 1 }])
170
+ end
171
+ end
172
+ end
173
+ ```
174
+
175
+ ### System Test Template
176
+
177
+ ```ruby
178
+ # test/system/posts_test.rb
179
+ require "application_system_test_case"
180
+
181
+ class PostsTest < ApplicationSystemTestCase
182
+ setup do
183
+ @user = users(:one)
184
+ sign_in_as @user
185
+ end
186
+
187
+ test "creating a post" do
188
+ visit new_post_url
189
+
190
+ fill_in "Title", with: "My Post"
191
+ fill_in "Body", with: "Post content here"
192
+ click_button "Create Post"
193
+
194
+ assert_text "Post created successfully"
195
+ assert_text "My Post"
196
+ end
197
+
198
+ test "shows validation errors" do
199
+ visit new_post_url
200
+ click_button "Create Post"
201
+
202
+ assert_text "can't be blank"
203
+ end
204
+ end
205
+ ```
206
+
207
+ ### ViewComponent Test Template
208
+
209
+ ```ruby
210
+ # test/components/status_badge_component_test.rb
211
+ require "test_helper"
212
+
213
+ class StatusBadgeComponentTest < ViewComponent::TestCase
214
+ test "renders published badge" do
215
+ render_inline(StatusBadgeComponent.new(status: :published))
216
+
217
+ assert_selector ".badge", text: "Published"
218
+ assert_selector ".bg-green-100"
219
+ end
220
+
221
+ test "renders draft badge" do
222
+ render_inline(StatusBadgeComponent.new(status: :draft))
223
+
224
+ assert_selector ".badge", text: "Draft"
225
+ assert_selector ".bg-gray-100"
226
+ end
227
+ end
228
+ ```
229
+
230
+ ### Policy Test Template
231
+
232
+ ```ruby
233
+ # test/policies/post_policy_test.rb
234
+ require "test_helper"
235
+
236
+ class PostPolicyTest < ActiveSupport::TestCase
237
+ setup do
238
+ @owner = users(:one)
239
+ @other = users(:two)
240
+ @post = posts(:one) # belongs to @owner
241
+ end
242
+
243
+ test "owner can update" do
244
+ assert PostPolicy.new(@owner, @post).update?
245
+ end
246
+
247
+ test "non-owner cannot update" do
248
+ assert_not PostPolicy.new(@other, @post).update?
249
+ end
250
+
251
+ test "scope returns only authorized records" do
252
+ scope = PostPolicy::Scope.new(@owner, Post).resolve
253
+ assert_includes scope, @post
254
+ end
255
+ end
256
+ ```
257
+
258
+ ## Step 4: Verify Failure
259
+
260
+ Run the test:
261
+ ```bash
262
+ bin/rails test test/models/post_test.rb --verbose
263
+ ```
264
+
265
+ The test MUST fail with a clear error. If it passes immediately, either:
266
+ - The behavior already exists
267
+ - The test isn't testing what you think
268
+
269
+ ## Step 5: Implement (GREEN)
270
+
271
+ Write the MINIMUM code to pass:
272
+ - No optimization
273
+ - No edge case handling beyond what's tested
274
+ - No refactoring
275
+ - Just make it work
276
+
277
+ ## Step 6: Verify Pass
278
+
279
+ ```bash
280
+ bin/rails test test/models/post_test.rb --verbose
281
+ ```
282
+
283
+ ## Step 7: Refactor
284
+
285
+ Improve code while tests stay green:
286
+ - Extract methods for clarity
287
+ - Improve naming
288
+ - Remove duplication
289
+ - Simplify logic
290
+
291
+ **Rule:** Make ONE change at a time, run tests after EACH change.
292
+
293
+ ## Step 8: Final Verification
294
+
295
+ Run all related tests:
296
+ ```bash
297
+ bin/rails test
298
+ ```
299
+
300
+ ## Fixtures Best Practices
301
+
302
+ ```yaml
303
+ # test/fixtures/posts.yml
304
+ published:
305
+ title: Published Post
306
+ body: This is published content
307
+ user: one
308
+ created_at: <%= 1.day.ago %>
309
+
310
+ draft:
311
+ title: Draft Post
312
+ body: This is draft content
313
+ user: one
314
+
315
+ recent:
316
+ title: Recent Post
317
+ body: Recent content
318
+ user: one
319
+ created_at: <%= 1.hour.ago %>
320
+
321
+ old:
322
+ title: Old Post
323
+ body: Old content
324
+ user: two
325
+ created_at: <%= 1.year.ago %>
326
+ ```
327
+
328
+ **Fixture naming tips:**
329
+ - Use descriptive names: `published`, `draft`, `admin_post`
330
+ - Reference other fixtures by name: `user: one`
331
+ - Use ERB for dynamic values: `<%= Time.current %>`
332
+
333
+ ## Test Helper Patterns
334
+
335
+ ```ruby
336
+ # test/test_helper.rb
337
+ class ActiveSupport::TestCase
338
+ # Use fixtures for all tests
339
+ fixtures :all
340
+
341
+ # Authentication helper
342
+ def sign_in_as(user)
343
+ post session_url, params: { email: user.email, password: "password" }
344
+ end
345
+
346
+ def sign_out
347
+ delete session_url
348
+ end
349
+ end
350
+ ```
351
+
352
+ ## Anti-Patterns to Avoid
353
+
354
+ 1. **Testing implementation, not behavior** — Test what it does, not how
355
+ 2. **Too many assertions per test** — One concept per test
356
+ 3. **Brittle tests** — Don't assert exact timestamps or error messages
357
+ 4. **Slow tests** — Prefer model tests over system tests when possible
358
+ 5. **Skipping the RED step** — Always see it fail first
359
+ 6. **Over-mocking** — Use real objects with fixtures when possible
@@ -0,0 +1,333 @@
1
+ ---
2
+ name: viewcomponent-patterns
3
+ description: Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # ViewComponent Patterns for Rails 8
8
+
9
+ ## Overview
10
+
11
+ ViewComponents are Ruby objects for building reusable, testable view components:
12
+ - Faster than partials (no partial lookup)
13
+ - Unit testable without full request cycle
14
+ - Encapsulate view logic with Ruby
15
+ - Type-safe with explicit interfaces
16
+
17
+ ## TDD Workflow
18
+
19
+ ```
20
+ ViewComponent Progress:
21
+ - [ ] Step 1: Write component test (RED)
22
+ - [ ] Step 2: Run test (fails - no component)
23
+ - [ ] Step 3: Generate component skeleton
24
+ - [ ] Step 4: Implement component
25
+ - [ ] Step 5: Run test (GREEN)
26
+ - [ ] Step 6: Add variants/slots if needed
27
+ ```
28
+
29
+ ## Step 1: Component Test (RED)
30
+
31
+ ```ruby
32
+ # test/components/card_component_test.rb
33
+ require "test_helper"
34
+
35
+ class CardComponentTest < ViewComponent::TestCase
36
+ test "renders the title" do
37
+ render_inline(CardComponent.new(title: "Test Title"))
38
+ assert_selector "h3", text: "Test Title"
39
+ end
40
+
41
+ test "renders content block" do
42
+ render_inline(CardComponent.new(title: "Title")) { "Card content" }
43
+ assert_text "Card content"
44
+ end
45
+
46
+ test "renders subtitle when provided" do
47
+ render_inline(CardComponent.new(title: "Title", subtitle: "Subtitle"))
48
+ assert_selector "p", text: "Subtitle"
49
+ end
50
+
51
+ test "does not render subtitle element when not provided" do
52
+ render_inline(CardComponent.new(title: "Title"))
53
+ assert_no_selector ".subtitle"
54
+ end
55
+ end
56
+ ```
57
+
58
+ ## Step 2-4: Implement Component
59
+
60
+ ### Base Component
61
+
62
+ ```ruby
63
+ # app/components/application_component.rb
64
+ class ApplicationComponent < ViewComponent::Base
65
+ include ActionView::Helpers::TagHelper
66
+ include ActionView::Helpers::NumberHelper
67
+
68
+ def not_specified_span
69
+ tag.span(I18n.t("components.common.not_specified"), class: "text-slate-400 italic")
70
+ end
71
+ end
72
+ ```
73
+
74
+ ### Basic Component
75
+
76
+ ```ruby
77
+ # app/components/card_component.rb
78
+ class CardComponent < ApplicationComponent
79
+ def initialize(title:, subtitle: nil)
80
+ @title = title
81
+ @subtitle = subtitle
82
+ end
83
+
84
+ attr_reader :title, :subtitle
85
+
86
+ def subtitle?
87
+ subtitle.present?
88
+ end
89
+ end
90
+ ```
91
+
92
+ ```erb
93
+ <%# app/components/card_component.html.erb %>
94
+ <div class="bg-white rounded-lg shadow p-6">
95
+ <h3 class="text-lg font-semibold text-slate-900"><%= title %></h3>
96
+ <% if subtitle? %>
97
+ <p class="subtitle text-sm text-slate-500"><%= subtitle %></p>
98
+ <% end %>
99
+ <div class="mt-4">
100
+ <%= content %>
101
+ </div>
102
+ </div>
103
+ ```
104
+
105
+ ## Common Patterns
106
+
107
+ ### Pattern 1: Status Badge
108
+
109
+ ```ruby
110
+ # app/components/badge_component.rb
111
+ class BadgeComponent < ApplicationComponent
112
+ VARIANTS = {
113
+ success: "bg-green-100 text-green-800",
114
+ warning: "bg-yellow-100 text-yellow-800",
115
+ error: "bg-red-100 text-red-800",
116
+ info: "bg-blue-100 text-blue-800",
117
+ neutral: "bg-slate-100 text-slate-800"
118
+ }.freeze
119
+
120
+ def initialize(text:, variant: :neutral)
121
+ @text = text
122
+ @variant = variant.to_sym
123
+ end
124
+
125
+ def call
126
+ tag.span(
127
+ @text,
128
+ class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{variant_classes}"
129
+ )
130
+ end
131
+
132
+ private
133
+
134
+ def variant_classes
135
+ VARIANTS.fetch(@variant, VARIANTS[:neutral])
136
+ end
137
+ end
138
+ ```
139
+
140
+ Testing:
141
+
142
+ ```ruby
143
+ # test/components/badge_component_test.rb
144
+ require "test_helper"
145
+
146
+ class BadgeComponentTest < ViewComponent::TestCase
147
+ test "renders success variant" do
148
+ render_inline(BadgeComponent.new(text: "Active", variant: :success))
149
+ assert_selector ".bg-green-100"
150
+ assert_text "Active"
151
+ end
152
+
153
+ test "renders error variant" do
154
+ render_inline(BadgeComponent.new(text: "Failed", variant: :error))
155
+ assert_selector ".bg-red-100"
156
+ end
157
+
158
+ test "defaults to neutral variant" do
159
+ render_inline(BadgeComponent.new(text: "Unknown"))
160
+ assert_selector ".bg-slate-100"
161
+ end
162
+ end
163
+ ```
164
+
165
+ ### Pattern 2: Component with Slots
166
+
167
+ ```ruby
168
+ # app/components/card_component.rb
169
+ class CardComponent < ApplicationComponent
170
+ renders_one :header
171
+ renders_one :footer
172
+ renders_many :actions
173
+
174
+ def initialize(title: nil)
175
+ @title = title
176
+ end
177
+ end
178
+ ```
179
+
180
+ Testing slots:
181
+
182
+ ```ruby
183
+ # test/components/card_component_test.rb
184
+ class CardComponentTest < ViewComponent::TestCase
185
+ test "renders header slot" do
186
+ render_inline(CardComponent.new) do |card|
187
+ card.with_header { "Custom Header" }
188
+ end
189
+
190
+ assert_text "Custom Header"
191
+ end
192
+
193
+ test "renders multiple action slots" do
194
+ render_inline(CardComponent.new) do |card|
195
+ card.with_action { "Action 1" }
196
+ card.with_action { "Action 2" }
197
+ end
198
+
199
+ assert_text "Action 1"
200
+ assert_text "Action 2"
201
+ end
202
+ end
203
+ ```
204
+
205
+ ### Pattern 3: Collection Component
206
+
207
+ ```ruby
208
+ # app/components/event_card_component.rb
209
+ class EventCardComponent < ApplicationComponent
210
+ with_collection_parameter :event
211
+
212
+ def initialize(event:)
213
+ @event = event
214
+ end
215
+
216
+ delegate :name, :event_date, :status, to: :@event
217
+
218
+ def formatted_date
219
+ return not_specified_span if event_date.nil?
220
+ I18n.l(event_date, format: :long)
221
+ end
222
+
223
+ def status_badge
224
+ render BadgeComponent.new(text: status.humanize, variant: status_variant)
225
+ end
226
+
227
+ private
228
+
229
+ def status_variant
230
+ case status.to_sym
231
+ when :confirmed then :success
232
+ when :cancelled then :error
233
+ when :pending then :warning
234
+ else :neutral
235
+ end
236
+ end
237
+ end
238
+ ```
239
+
240
+ Testing collections:
241
+
242
+ ```ruby
243
+ # test/components/event_card_component_test.rb
244
+ class EventCardComponentTest < ViewComponent::TestCase
245
+ test "renders single event" do
246
+ event = events(:one)
247
+ render_inline(EventCardComponent.new(event: event))
248
+ assert_text event.name
249
+ end
250
+
251
+ test "renders collection" do
252
+ events_list = [events(:one), events(:two)]
253
+ render_inline(EventCardComponent.with_collection(events_list))
254
+ assert_selector ".event-card", count: 2
255
+ end
256
+ end
257
+ ```
258
+
259
+ ### Pattern 4: Modal Component
260
+
261
+ ```ruby
262
+ # app/components/modal_component.rb
263
+ class ModalComponent < ApplicationComponent
264
+ renders_one :trigger
265
+ renders_one :title
266
+ renders_one :footer
267
+
268
+ def initialize(id:, size: :medium)
269
+ @id = id
270
+ @size = size
271
+ end
272
+
273
+ def size_classes
274
+ case @size
275
+ when :small then "max-w-md"
276
+ when :medium then "max-w-lg"
277
+ when :large then "max-w-2xl"
278
+ when :full then "max-w-full mx-4"
279
+ end
280
+ end
281
+ end
282
+ ```
283
+
284
+ ## Usage in Views
285
+
286
+ ```erb
287
+ <%# Simple component %>
288
+ <%= render BadgeComponent.new(text: "Active", variant: :success) %>
289
+
290
+ <%# Component with block %>
291
+ <%= render CardComponent.new(title: "Stats") do %>
292
+ <p>Content here</p>
293
+ <% end %>
294
+
295
+ <%# Component with slots %>
296
+ <%= render CardComponent.new do |card| %>
297
+ <% card.with_header do %>
298
+ <h2>Header</h2>
299
+ <% end %>
300
+ Content
301
+ <% end %>
302
+
303
+ <%# Collection %>
304
+ <%= render EventCardComponent.with_collection(@events) %>
305
+ ```
306
+
307
+ ## Previews (Development)
308
+
309
+ ```ruby
310
+ # test/components/previews/badge_component_preview.rb
311
+ class BadgeComponentPreview < ViewComponent::Preview
312
+ def success
313
+ render BadgeComponent.new(text: "Active", variant: :success)
314
+ end
315
+
316
+ def error
317
+ render BadgeComponent.new(text: "Failed", variant: :error)
318
+ end
319
+ end
320
+ ```
321
+
322
+ Access at: `http://localhost:3000/rails/view_components`
323
+
324
+ ## Checklist
325
+
326
+ - [ ] Test written first (RED)
327
+ - [ ] Extends `ApplicationComponent`
328
+ - [ ] Uses slots for flexible content
329
+ - [ ] Variants use constants (Open/Closed)
330
+ - [ ] Tested with different inputs
331
+ - [ ] Collection rendering tested
332
+ - [ ] Preview created for development
333
+ - [ ] All tests GREEN
data/.gitignore CHANGED
@@ -14,3 +14,4 @@
14
14
  /test/lib/tmp/install_generator/config/routes.rb
15
15
  /app/assets/builds/*
16
16
  !/app/assets/builds/.keep
17
+ !/app/assets/builds/source_monitor/
data/.rubocop.yml CHANGED
@@ -4,6 +4,8 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
4
4
  AllCops:
5
5
  Exclude:
6
6
  - "test/dummy/db/schema.rb"
7
+ - "test/tmp/**/*"
8
+ - "test/lib/tmp/**/*"
7
9
 
8
10
  # Overwrite or add rules to create your own house style
9
11
  #
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.4
1
+ 4.0.1