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,321 @@
1
+ ---
2
+ name: rails-model-generator
3
+ description: Creates Rails models using TDD approach - test first, then migration, then model. Use when creating new models, adding model validations, defining associations, or setting up database tables.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Model Generator (TDD Approach)
8
+
9
+ ## Overview
10
+
11
+ This skill creates models the TDD way:
12
+ 1. Define requirements (attributes, validations, associations)
13
+ 2. Write model test with expected behavior (RED)
14
+ 3. Create fixtures for test data
15
+ 4. Generate migration
16
+ 5. Implement model to pass tests (GREEN)
17
+ 6. Refactor if needed
18
+
19
+ ## Workflow Checklist
20
+
21
+ ```
22
+ Model Creation Progress:
23
+ - [ ] Step 1: Define requirements (attributes, validations, associations)
24
+ - [ ] Step 2: Create model test (RED)
25
+ - [ ] Step 3: Create fixtures
26
+ - [ ] Step 4: Run test (should fail - no model/table)
27
+ - [ ] Step 5: Generate migration
28
+ - [ ] Step 6: Run migration
29
+ - [ ] Step 7: Create model file (empty)
30
+ - [ ] Step 8: Run test (should fail - no validations)
31
+ - [ ] Step 9: Add validations and associations
32
+ - [ ] Step 10: Run test (GREEN)
33
+ ```
34
+
35
+ ## Step 1: Requirements Template
36
+
37
+ Before writing code, define the model:
38
+
39
+ ```markdown
40
+ ## Model: [ModelName]
41
+
42
+ ### Table: [table_name]
43
+
44
+ ### Attributes
45
+ | Name | Type | Constraints | Default |
46
+ |------|------|-------------|---------|
47
+ | name | string | required, unique | - |
48
+ | email | string | required, unique, email format | - |
49
+ | status | integer | enum | 0 (pending) |
50
+ | organization_id | bigint | foreign key | - |
51
+
52
+ ### Associations
53
+ - belongs_to :organization
54
+ - has_many :posts, dependent: :destroy
55
+ - has_one :profile, dependent: :destroy
56
+
57
+ ### Validations
58
+ - name: presence, uniqueness, length(max: 100)
59
+ - email: presence, uniqueness, format(email)
60
+ - status: inclusion in enum values
61
+
62
+ ### Scopes
63
+ - active: status = active
64
+ - recent: ordered by created_at desc
65
+ - by_organization(org): where organization_id = org.id
66
+
67
+ ### Instance Methods
68
+ - full_name: combines first_name and last_name
69
+ - active?: checks if status is active
70
+
71
+ ### Callbacks
72
+ - before_save :normalize_email
73
+ - after_create :send_welcome_email
74
+ ```
75
+
76
+ ## Step 2: Create Model Test
77
+
78
+ Location: `test/models/[model_name]_test.rb`
79
+
80
+ ```ruby
81
+ # frozen_string_literal: true
82
+
83
+ require "test_helper"
84
+
85
+ class ModelNameTest < ActiveSupport::TestCase
86
+ # === Associations ===
87
+ test "belongs to organization" do
88
+ model = model_names(:one)
89
+ assert_respond_to model, :organization
90
+ assert_instance_of Organization, model.organization
91
+ end
92
+
93
+ test "has many posts" do
94
+ model = model_names(:one)
95
+ assert_respond_to model, :posts
96
+ end
97
+
98
+ # === Validations ===
99
+ test "requires name" do
100
+ model = ModelName.new(name: nil)
101
+ assert_not model.valid?
102
+ assert_includes model.errors[:name], "can't be blank"
103
+ end
104
+
105
+ test "requires unique email (case insensitive)" do
106
+ existing = model_names(:one)
107
+ model = ModelName.new(email: existing.email.upcase)
108
+ assert_not model.valid?
109
+ assert_includes model.errors[:email], "has already been taken"
110
+ end
111
+
112
+ test "validates name length max 100" do
113
+ model = ModelName.new(name: "a" * 101)
114
+ assert_not model.valid?
115
+ assert model.errors[:name].any? { |e| e.include?("too long") }
116
+ end
117
+
118
+ # === Scopes ===
119
+ test ".active returns only active records" do
120
+ active_record = model_names(:active_one)
121
+ inactive_record = model_names(:inactive_one)
122
+
123
+ results = ModelName.active
124
+ assert_includes results, active_record
125
+ assert_not_includes results, inactive_record
126
+ end
127
+
128
+ # === Instance Methods ===
129
+ test "#full_name returns combined name" do
130
+ model = ModelName.new(first_name: "John", last_name: "Doe")
131
+ assert_equal "John Doe", model.full_name
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## Step 3: Create Fixtures
137
+
138
+ Location: `test/fixtures/[model_name_plural].yml`
139
+
140
+ ```yaml
141
+ # test/fixtures/model_names.yml
142
+ one:
143
+ name: "Test Model One"
144
+ email: "model-one@example.com"
145
+ status: 0
146
+ organization: one
147
+
148
+ two:
149
+ name: "Test Model Two"
150
+ email: "model-two@example.com"
151
+ status: 0
152
+ organization: one
153
+
154
+ active_one:
155
+ name: "Active Model"
156
+ email: "active@example.com"
157
+ status: 1
158
+ organization: one
159
+
160
+ inactive_one:
161
+ name: "Inactive Model"
162
+ email: "inactive@example.com"
163
+ status: 2
164
+ organization: one
165
+ ```
166
+
167
+ ## Step 4: Run Test (Verify RED)
168
+
169
+ ```bash
170
+ bin/rails test test/models/model_name_test.rb
171
+ ```
172
+
173
+ Expected: Failure because model/table doesn't exist.
174
+
175
+ ## Step 5: Generate Migration
176
+
177
+ ```bash
178
+ bin/rails generate migration CreateModelNames \
179
+ name:string \
180
+ email:string:uniq \
181
+ status:integer \
182
+ organization:references
183
+ ```
184
+
185
+ Review the generated migration and add:
186
+ - Null constraints: `null: false`
187
+ - Defaults: `default: 0`
188
+ - Indexes: `add_index :table, :column`
189
+
190
+ ```ruby
191
+ # db/migrate/YYYYMMDDHHMMSS_create_model_names.rb
192
+ class CreateModelNames < ActiveRecord::Migration[8.0]
193
+ def change
194
+ create_table :model_names do |t|
195
+ t.string :name, null: false
196
+ t.string :email, null: false
197
+ t.integer :status, null: false, default: 0
198
+ t.references :organization, null: false, foreign_key: true
199
+
200
+ t.timestamps
201
+ end
202
+
203
+ add_index :model_names, :email, unique: true
204
+ add_index :model_names, :status
205
+ end
206
+ end
207
+ ```
208
+
209
+ ## Step 6: Run Migration
210
+
211
+ ```bash
212
+ bin/rails db:migrate
213
+ ```
214
+
215
+ Verify with:
216
+ ```bash
217
+ bin/rails db:migrate:status
218
+ ```
219
+
220
+ ## Step 7: Create Model File
221
+
222
+ Location: `app/models/[model_name].rb`
223
+
224
+ ```ruby
225
+ # frozen_string_literal: true
226
+
227
+ class ModelName < ApplicationRecord
228
+ end
229
+ ```
230
+
231
+ ## Step 8: Run Test (Still RED)
232
+
233
+ ```bash
234
+ bin/rails test test/models/model_name_test.rb
235
+ ```
236
+
237
+ Expected: Failures for missing validations/associations.
238
+
239
+ ## Step 9: Add Validations & Associations
240
+
241
+ ```ruby
242
+ # frozen_string_literal: true
243
+
244
+ class ModelName < ApplicationRecord
245
+ # === Associations ===
246
+ belongs_to :organization
247
+ has_many :posts, dependent: :destroy
248
+
249
+ # === Enums ===
250
+ enum :status, { pending: 0, active: 1, suspended: 2 }
251
+
252
+ # === Validations ===
253
+ validates :name, presence: true,
254
+ uniqueness: true,
255
+ length: { maximum: 100 }
256
+ validates :email, presence: true,
257
+ uniqueness: { case_sensitive: false },
258
+ format: { with: URI::MailTo::EMAIL_REGEXP }
259
+
260
+ # === Scopes ===
261
+ scope :active, -> { where(status: :active) }
262
+ scope :recent, -> { order(created_at: :desc) }
263
+
264
+ # === Instance Methods ===
265
+ def full_name
266
+ "#{first_name} #{last_name}".strip
267
+ end
268
+ end
269
+ ```
270
+
271
+ ## Step 10: Run Test (GREEN)
272
+
273
+ ```bash
274
+ bin/rails test test/models/model_name_test.rb
275
+ ```
276
+
277
+ All tests should pass.
278
+
279
+ ## References
280
+
281
+ - See [reference/validations.md](reference/validations.md) for validation patterns
282
+
283
+ ## Common Patterns
284
+
285
+ ### Enum with Validation
286
+
287
+ ```ruby
288
+ enum :status, { draft: 0, published: 1, archived: 2 }
289
+ validates :status, inclusion: { in: statuses.keys }
290
+ ```
291
+
292
+ ### Polymorphic Association
293
+
294
+ ```ruby
295
+ belongs_to :commentable, polymorphic: true
296
+ ```
297
+
298
+ ### Counter Cache
299
+
300
+ ```ruby
301
+ belongs_to :organization, counter_cache: true
302
+ # Add: organization.posts_count column
303
+ ```
304
+
305
+ ### Soft Delete
306
+
307
+ ```ruby
308
+ scope :active, -> { where(deleted_at: nil) }
309
+ scope :deleted, -> { where.not(deleted_at: nil) }
310
+
311
+ def soft_delete
312
+ update(deleted_at: Time.current)
313
+ end
314
+ ```
315
+
316
+ ### Normalizes (Rails 7.1+)
317
+
318
+ ```ruby
319
+ normalizes :email, with: -> { _1.strip.downcase }
320
+ normalizes :phone, with: -> { _1.gsub(/\D/, "") }
321
+ ```
@@ -0,0 +1,298 @@
1
+ # Rails Validation Patterns Reference
2
+
3
+ ## Standard Validations
4
+
5
+ ### Presence
6
+
7
+ ```ruby
8
+ validates :name, presence: true
9
+ validates :email, presence: { message: "is required" }
10
+ ```
11
+
12
+ **Test:**
13
+ ```ruby
14
+ test "requires name" do
15
+ record = Model.new(valid_attributes.except(:name))
16
+ assert_not record.valid?
17
+ assert record.errors[:name].any?
18
+ end
19
+ ```
20
+
21
+ ### Uniqueness
22
+
23
+ ```ruby
24
+ validates :email, uniqueness: true
25
+ validates :email, uniqueness: { case_sensitive: false }
26
+ validates :slug, uniqueness: { scope: :organization_id }
27
+ validates :email, uniqueness: { conditions: -> { where(deleted_at: nil) } }
28
+ ```
29
+
30
+ **Test:**
31
+ ```ruby
32
+ test "requires unique email" do
33
+ existing = users(:one)
34
+ record = User.new(email: existing.email, password: "password123", account: accounts(:one))
35
+ assert_not record.valid?
36
+ assert record.errors[:email].any?
37
+ end
38
+
39
+ test "requires unique slug scoped to organization" do
40
+ existing = records(:one)
41
+ record = Record.new(slug: existing.slug, organization: existing.organization)
42
+ assert_not record.valid?
43
+ assert record.errors[:slug].any?
44
+ end
45
+ ```
46
+
47
+ ### Length
48
+
49
+ ```ruby
50
+ validates :name, length: { maximum: 100 }
51
+ validates :bio, length: { minimum: 10, maximum: 500 }
52
+ validates :pin, length: { is: 4 }
53
+ validates :tags, length: { in: 1..5 }
54
+ ```
55
+
56
+ **Test:**
57
+ ```ruby
58
+ test "rejects name longer than 100 characters" do
59
+ record = Model.new(valid_attributes.merge(name: "a" * 101))
60
+ assert_not record.valid?
61
+ assert record.errors[:name].any?
62
+ end
63
+
64
+ test "accepts name within 100 characters" do
65
+ record = Model.new(valid_attributes.merge(name: "a" * 100))
66
+ assert record.valid?
67
+ end
68
+ ```
69
+
70
+ ### Format
71
+
72
+ ```ruby
73
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
74
+ validates :phone, format: { with: /\A\+?[\d\s-]+\z/ }
75
+ validates :slug, format: { with: /\A[a-z0-9-]+\z/, message: "only allows lowercase letters, numbers, and hyphens" }
76
+ ```
77
+
78
+ **Test:**
79
+ ```ruby
80
+ test "accepts valid email format" do
81
+ record = Model.new(valid_attributes.merge(email: "test@example.com"))
82
+ assert record.valid?
83
+ end
84
+
85
+ test "rejects invalid email format" do
86
+ record = Model.new(valid_attributes.merge(email: "invalid-email"))
87
+ assert_not record.valid?
88
+ assert record.errors[:email].any?
89
+ end
90
+ ```
91
+
92
+ ### Numericality
93
+
94
+ ```ruby
95
+ validates :age, numericality: { only_integer: true, greater_than: 0 }
96
+ validates :price, numericality: { greater_than_or_equal_to: 0 }
97
+ validates :quantity, numericality: { only_integer: true, in: 1..100 }
98
+ ```
99
+
100
+ **Test:**
101
+ ```ruby
102
+ test "requires positive integer for age" do
103
+ record = Model.new(valid_attributes.merge(age: -1))
104
+ assert_not record.valid?
105
+ assert record.errors[:age].any?
106
+ end
107
+
108
+ test "rejects non-integer age" do
109
+ record = Model.new(valid_attributes.merge(age: 1.5))
110
+ assert_not record.valid?
111
+ end
112
+ ```
113
+
114
+ ### Inclusion/Exclusion
115
+
116
+ ```ruby
117
+ validates :status, inclusion: { in: %w[draft published archived] }
118
+ validates :role, inclusion: { in: :allowed_roles }
119
+ validates :username, exclusion: { in: %w[admin root system] }
120
+ ```
121
+
122
+ **Test:**
123
+ ```ruby
124
+ test "accepts valid status values" do
125
+ %w[draft published archived].each do |status|
126
+ record = Model.new(valid_attributes.merge(status: status))
127
+ assert record.valid?, "Expected #{status} to be valid"
128
+ end
129
+ end
130
+
131
+ test "rejects invalid status values" do
132
+ record = Model.new(valid_attributes.merge(status: "invalid"))
133
+ assert_not record.valid?
134
+ assert record.errors[:status].any?
135
+ end
136
+
137
+ test "rejects reserved usernames" do
138
+ %w[admin root system].each do |username|
139
+ record = Model.new(valid_attributes.merge(username: username))
140
+ assert_not record.valid?, "Expected #{username} to be invalid"
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### Acceptance
146
+
147
+ ```ruby
148
+ validates :terms, acceptance: true
149
+ validates :terms, acceptance: { accept: ['yes', 'true', '1'] }
150
+ ```
151
+
152
+ ### Confirmation
153
+
154
+ ```ruby
155
+ validates :password, confirmation: true
156
+ # Requires :password_confirmation attribute in form
157
+ ```
158
+
159
+ ## Conditional Validations
160
+
161
+ ### With If/Unless
162
+
163
+ ```ruby
164
+ validates :phone, presence: true, if: :requires_phone?
165
+ validates :company, presence: true, unless: :individual?
166
+ validates :bio, length: { minimum: 50 }, if: -> { featured? }
167
+ ```
168
+
169
+ **Test:**
170
+ ```ruby
171
+ test "requires phone when requires_phone? is true" do
172
+ record = Model.new(valid_attributes.except(:phone))
173
+ record.stub(:requires_phone?, true) do
174
+ assert_not record.valid?
175
+ assert record.errors[:phone].any?
176
+ end
177
+ end
178
+
179
+ test "does not require phone when requires_phone? is false" do
180
+ record = Model.new(valid_attributes.except(:phone))
181
+ record.stub(:requires_phone?, false) do
182
+ assert record.valid?
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### With On (Context)
188
+
189
+ ```ruby
190
+ validates :password, presence: true, on: :create
191
+ validates :reason, presence: true, on: :archive
192
+ ```
193
+
194
+ **Test:**
195
+ ```ruby
196
+ test "requires password on create" do
197
+ record = Model.new(valid_attributes.except(:password))
198
+ assert_not record.valid?
199
+ assert record.errors[:password].any?
200
+ end
201
+ ```
202
+
203
+ ## Custom Validations
204
+
205
+ ### Custom Method
206
+
207
+ ```ruby
208
+ class User < ApplicationRecord
209
+ validate :email_domain_allowed
210
+
211
+ private
212
+
213
+ def email_domain_allowed
214
+ return if email.blank?
215
+
216
+ domain = email.split('@').last
217
+ unless allowed_domains.include?(domain)
218
+ errors.add(:email, "domain is not allowed")
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ **Test:**
225
+ ```ruby
226
+ test "accepts allowed email domain" do
227
+ user = User.new(valid_attributes.merge(email: "test@allowed.com"))
228
+ assert user.valid?
229
+ end
230
+
231
+ test "rejects disallowed email domain" do
232
+ user = User.new(valid_attributes.merge(email: "test@blocked.com"))
233
+ assert_not user.valid?
234
+ assert_includes user.errors[:email], "domain is not allowed"
235
+ end
236
+ ```
237
+
238
+ ### Custom Validator Class
239
+
240
+ ```ruby
241
+ # app/validators/email_domain_validator.rb
242
+ class EmailDomainValidator < ActiveModel::EachValidator
243
+ def validate_each(record, attribute, value)
244
+ return if value.blank?
245
+
246
+ domain = value.split('@').last
247
+ unless options[:allowed].include?(domain)
248
+ record.errors.add(attribute, options[:message] || "domain not allowed")
249
+ end
250
+ end
251
+ end
252
+
253
+ # Usage in model:
254
+ validates :email, email_domain: { allowed: %w[company.com], message: "must be company email" }
255
+ ```
256
+
257
+ ## Association Validations
258
+
259
+ ```ruby
260
+ validates :organization, presence: true
261
+ validates_associated :profile # Validates the associated record too
262
+
263
+ # With nested attributes
264
+ accepts_nested_attributes_for :addresses, allow_destroy: true
265
+ validates :addresses, length: { minimum: 1, message: "must have at least one address" }
266
+ ```
267
+
268
+ ## Database-Level Constraints
269
+
270
+ Always pair validations with database constraints:
271
+
272
+ ```ruby
273
+ # Migration
274
+ add_column :users, :email, :string, null: false
275
+ add_index :users, :email, unique: true
276
+ add_check_constraint :users, 'age >= 0', name: 'age_non_negative'
277
+
278
+ # Model
279
+ validates :email, presence: true, uniqueness: true
280
+ validates :age, numericality: { greater_than_or_equal_to: 0 }
281
+ ```
282
+
283
+ ## Common Email Regex Patterns
284
+
285
+ ```ruby
286
+ # Simple (recommended for most cases)
287
+ URI::MailTo::EMAIL_REGEXP
288
+
289
+ # More permissive
290
+ /\A[^@\s]+@[^@\s]+\z/
291
+ ```
292
+
293
+ ## Performance Tips
294
+
295
+ 1. **Order validations by cost**: Put cheap validations first
296
+ 2. **Use `on:` to skip validations**: Don't validate password on every save
297
+ 3. **Avoid N+1 in custom validations**: Cache lookups
298
+ 4. **Use database constraints**: They're faster than Rails validations