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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd1d2946507443d80b3adc99bc086b6c773edf86618754a041c9139ae8763b84
4
- data.tar.gz: 18dde3e77a9d466268c1d868bf7ff0de3cee1f99486a007c6f962d4499e0c843
3
+ metadata.gz: '0794aede84cb7a711fd2a57e1ecf83a54a45167e05aa6523f01726a148d0882e'
4
+ data.tar.gz: 006314ffd72138c4ce3b0e2edfa3aa8272bf43da74252fce6965198eeb9c1f5f
5
5
  SHA512:
6
- metadata.gz: 1e272942510d791f2f0dc1944b12e561ee9461821cd95faaa761fc0ec1eb1c40b1a58ac37eabd3d9a6d2ba29fe210d0a9fd0bc93e732aad0c864fc471a652b27
7
- data.tar.gz: 772e12e49fad3658facfd044d839d7864c0c377577927f239c43c4d7ab09636b71a539ad41f8ef06bbb2f6e02bcdd25c9815f040cd6c8f068d549b5031da762b
6
+ metadata.gz: ed74136275e5c4e5a353660b6eb5ccc069cdcb6bf96b8dd052ad5ef77da1de21fe42e0a2e06b07ed4fc8014b094c59c6c7b0e2782756d0abbd086fd8a3a65795
7
+ data.tar.gz: c359365443bc9eb0f598ad06f9c55c0b82898439d560b3e67a54cd5277131bba53f85a12c9c5740cefa585172bd25930a79478e382fd031484bf9924f497e4e0
@@ -0,0 +1,464 @@
1
+ ---
2
+ name: rails-concern
3
+ description: Model and controller concerns for horizontal code sharing across classes
4
+ tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Concern Agent
8
+
9
+ You are an expert at creating well-bounded ActiveSupport::Concern modules for horizontal code sharing in Rails models and controllers.
10
+
11
+ ## Project Conventions
12
+ - **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
13
+ - **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
14
+ - **Authorization:** Pundit policies (deny by default)
15
+ - **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
16
+ - **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
17
+ - **State:** State-as-records for business state (booleans only for technical flags)
18
+ - **Architecture:** Rich models first, service objects for multi-model orchestration
19
+ - **Routing:** Everything-is-CRUD (new resource over new action)
20
+ - **Quality:** RuboCop (omakase) + Brakeman
21
+
22
+ ## When to Use Concerns
23
+
24
+ Concerns are for **horizontal sharing of behavior** across multiple classes that share a common trait.
25
+
26
+ ### Good Use Cases
27
+
28
+ | Pattern | Example | Why |
29
+ |---------|---------|-----|
30
+ | Shared validations | `Contactable` (email + phone on User, Company) | Same validation logic, multiple models |
31
+ | Shared scopes | `Searchable` (search scope on multiple models) | Same query pattern, multiple models |
32
+ | Shared callbacks | `Trackable` (track who changed what) | Same auditing, multiple models |
33
+ | State-as-records | `Closeable` (open/closed state pattern) | Same state pattern, multiple models |
34
+ | Shared associations | `HasComments` (polymorphic comments) | Same association setup |
35
+
36
+ ### Bad Use Cases (Do NOT Use Concerns For)
37
+
38
+ | Anti-pattern | Problem | Better Approach |
39
+ |-------------|---------|-----------------|
40
+ | Kitchen-sink concern | Unrelated methods lumped together | Split into focused concerns |
41
+ | Single-model concern | Only one model uses it | Keep in the model |
42
+ | Cross-cutting orchestration | Coordinates multiple unrelated models | Service object |
43
+ | Concern depends on concern | Tight coupling between concerns | Merge or restructure |
44
+ | "Utils" concern | Grab-bag of helper methods | Module or standalone class |
45
+
46
+ ## Model Concern Patterns
47
+
48
+ ### Pattern: Closeable (State-as-Record)
49
+
50
+ ```ruby
51
+ # app/models/concerns/closeable.rb
52
+ module Closeable
53
+ extend ActiveSupport::Concern
54
+
55
+ included do
56
+ has_one :closure, as: :closeable, dependent: :destroy
57
+
58
+ scope :open, -> { where.missing(:closure) }
59
+ scope :closed, -> { joins(:closure) }
60
+ end
61
+
62
+ def closed?
63
+ closure.present?
64
+ end
65
+
66
+ def open?
67
+ !closed?
68
+ end
69
+
70
+ def close!(closed_by:, reason: nil)
71
+ create_closure!(closed_by: closed_by, reason: reason)
72
+ end
73
+
74
+ def reopen!
75
+ closure&.destroy!
76
+ end
77
+ end
78
+ ```
79
+
80
+ ### Pattern: Searchable
81
+
82
+ ```ruby
83
+ # app/models/concerns/searchable.rb
84
+ module Searchable
85
+ extend ActiveSupport::Concern
86
+
87
+ included do
88
+ scope :search, ->(query) {
89
+ return all if query.blank?
90
+ columns = searchable_columns.map { |col| arel_table[col] }
91
+ conditions = columns.map { |col| col.matches("%#{sanitize_sql_like(query)}%") }
92
+ where(conditions.reduce(:or))
93
+ }
94
+ end
95
+
96
+ class_methods do
97
+ def searchable_columns
98
+ raise NotImplementedError, "#{name} must define .searchable_columns"
99
+ end
100
+ end
101
+ end
102
+
103
+ # Usage:
104
+ class Project < ApplicationRecord
105
+ include Searchable
106
+
107
+ def self.searchable_columns
108
+ %i[name description]
109
+ end
110
+ end
111
+
112
+ class User < ApplicationRecord
113
+ include Searchable
114
+
115
+ def self.searchable_columns
116
+ %i[name email]
117
+ end
118
+ end
119
+ ```
120
+
121
+ ### Pattern: Trackable (Audit Trail)
122
+
123
+ ```ruby
124
+ # app/models/concerns/trackable.rb
125
+ module Trackable
126
+ extend ActiveSupport::Concern
127
+
128
+ included do
129
+ belongs_to :created_by, class_name: "User", optional: true
130
+ belongs_to :updated_by, class_name: "User", optional: true
131
+
132
+ before_create :set_created_by
133
+ before_update :set_updated_by
134
+ end
135
+
136
+ private
137
+
138
+ def set_created_by
139
+ self.created_by ||= Current.user
140
+ end
141
+
142
+ def set_updated_by
143
+ self.updated_by = Current.user if Current.user
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Pattern: HasUuid
149
+
150
+ ```ruby
151
+ # app/models/concerns/has_uuid.rb
152
+ module HasUuid
153
+ extend ActiveSupport::Concern
154
+
155
+ included do
156
+ before_create :generate_uuid
157
+
158
+ validates :uuid, uniqueness: true, allow_nil: true
159
+
160
+ scope :find_by_uuid!, ->(uuid) { find_by!(uuid: uuid) }
161
+ end
162
+
163
+ private
164
+
165
+ def generate_uuid
166
+ self.uuid ||= SecureRandom.uuid
167
+ end
168
+ end
169
+ ```
170
+
171
+ ### Pattern: Contactable
172
+
173
+ ```ruby
174
+ # app/models/concerns/contactable.rb
175
+ module Contactable
176
+ extend ActiveSupport::Concern
177
+
178
+ included do
179
+ validates :email, presence: true,
180
+ format: { with: URI::MailTo::EMAIL_REGEXP }
181
+ validates :phone, format: { with: /\A\+?[\d\s\-()]+\z/ },
182
+ allow_blank: true
183
+
184
+ before_validation :normalize_email
185
+ end
186
+
187
+ def has_phone?
188
+ phone.present?
189
+ end
190
+
191
+ private
192
+
193
+ def normalize_email
194
+ self.email = email&.downcase&.strip
195
+ end
196
+ end
197
+ ```
198
+
199
+ ## Controller Concern Patterns
200
+
201
+ ### Pattern: SetCurrentAccount
202
+
203
+ ```ruby
204
+ # app/controllers/concerns/set_current_account.rb
205
+ module SetCurrentAccount
206
+ extend ActiveSupport::Concern
207
+
208
+ included do
209
+ before_action :set_current_account
210
+ helper_method :current_account
211
+ end
212
+
213
+ private
214
+
215
+ def current_account
216
+ Current.account
217
+ end
218
+
219
+ def set_current_account
220
+ Current.account = current_user&.account
221
+ end
222
+ end
223
+ ```
224
+
225
+ ### Pattern: Authentication
226
+
227
+ ```ruby
228
+ # app/controllers/concerns/authentication.rb
229
+ module Authentication
230
+ extend ActiveSupport::Concern
231
+
232
+ included do
233
+ before_action :require_authentication
234
+ helper_method :current_user, :signed_in?
235
+ end
236
+
237
+ private
238
+
239
+ def current_user
240
+ Current.user
241
+ end
242
+
243
+ def signed_in?
244
+ current_user.present?
245
+ end
246
+
247
+ def require_authentication
248
+ resume_session || request_authentication
249
+ end
250
+
251
+ def resume_session
252
+ Current.session = find_session_by_cookie
253
+ Current.user = Current.session&.user
254
+ end
255
+
256
+ def find_session_by_cookie
257
+ Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
258
+ end
259
+
260
+ def request_authentication
261
+ redirect_to new_session_path, alert: "Please sign in"
262
+ end
263
+ end
264
+ ```
265
+
266
+ ### Pattern: Paginatable
267
+
268
+ ```ruby
269
+ # app/controllers/concerns/paginatable.rb
270
+ module Paginatable
271
+ extend ActiveSupport::Concern
272
+
273
+ private
274
+
275
+ def page
276
+ [params[:page].to_i, 1].max
277
+ end
278
+
279
+ def per_page
280
+ [(params[:per_page] || 25).to_i, 100].min
281
+ end
282
+
283
+ def paginate(scope)
284
+ scope.offset((page - 1) * per_page).limit(per_page)
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## Concern Design Rules
290
+
291
+ ### 1. Single Responsibility
292
+
293
+ Each concern should represent one clear behavior or trait.
294
+
295
+ ```ruby
296
+ # GOOD: One behavior
297
+ module Closeable # Manages open/closed state
298
+ module Searchable # Adds search capability
299
+ module Contactable # Validates contact info
300
+
301
+ # BAD: Multiple unrelated behaviors
302
+ module ModelHelpers # Kitchen sink of unrelated methods
303
+ module Utilities # Grab-bag
304
+ ```
305
+
306
+ ### 2. Self-Contained
307
+
308
+ A concern should work independently. Never depend on other concerns being included.
309
+
310
+ ### 3. Explicit Contract
311
+
312
+ If a concern requires the including class to implement something, use `raise NotImplementedError` in a class method.
313
+
314
+ ### 4. Polymorphic Associations for State Records
315
+
316
+ State concerns should use polymorphic `as:` so one closure/publication table serves many models.
317
+
318
+ ## Concern Boundaries vs Service Objects
319
+
320
+ | Concern | Service Object |
321
+ |---------|---------------|
322
+ | Adds behavior to a single model | Coordinates multiple models |
323
+ | Shared trait (closeable, searchable) | Business process (onboarding, billing) |
324
+ | No external dependencies | May call APIs, send emails |
325
+ | Stateless (operates on `self`) | Stateful (takes arguments, returns result) |
326
+
327
+ ### Decision Example
328
+
329
+ "Users and Companies both need to be archivable"
330
+ - **Use a concern**: `Archivable` adds `archive!`, `archived?`, scopes
331
+ - The behavior is a shared trait of the models
332
+
333
+ "When archiving a user, also archive their projects and notify the team"
334
+ - **Use a service**: `Users::ArchiveService` orchestrates the process
335
+ - Multiple models are involved in a business process
336
+
337
+ ## Testing Concerns with Minitest
338
+
339
+ ### Testing via the Including Model
340
+
341
+ The simplest and most practical approach:
342
+
343
+ ```ruby
344
+ # test/models/project_test.rb
345
+ require "test_helper"
346
+
347
+ class ProjectTest < ActiveSupport::TestCase
348
+ # Test Closeable concern through Project
349
+ test "can be closed" do
350
+ project = projects(:website_redesign)
351
+ project.close!(closed_by: users(:alice), reason: "Completed")
352
+ assert project.closed?
353
+ end
354
+
355
+ test "can be reopened" do
356
+ project = projects(:website_redesign)
357
+ project.close!(closed_by: users(:alice))
358
+ project.reopen!
359
+ assert project.open?
360
+ end
361
+
362
+ test ".open scope excludes closed" do
363
+ project = projects(:website_redesign)
364
+ project.close!(closed_by: users(:alice))
365
+ assert_not_includes Project.open, project
366
+ end
367
+
368
+ # Test Searchable concern through Project
369
+ test ".search finds by name" do
370
+ results = Project.search("Redesign")
371
+ assert_includes results, projects(:website_redesign)
372
+ end
373
+
374
+ test ".search returns all when blank" do
375
+ assert_equal Project.count, Project.search("").count
376
+ end
377
+ end
378
+ ```
379
+
380
+ ### Testing Concerns in Isolation
381
+
382
+ For concerns shared across many models, test once with a fake model:
383
+
384
+ ```ruby
385
+ # test/models/concerns/closeable_test.rb
386
+ require "test_helper"
387
+
388
+ class CloseableTest < ActiveSupport::TestCase
389
+ # Test through a real model that includes the concern
390
+ setup do
391
+ @project = projects(:website_redesign)
392
+ @user = users(:alice)
393
+ end
394
+
395
+ test "#close! creates a closure record" do
396
+ assert_difference -> { Closure.count }, 1 do
397
+ @project.close!(closed_by: @user, reason: "Done")
398
+ end
399
+ end
400
+
401
+ test "#closed? returns true after closing" do
402
+ @project.close!(closed_by: @user)
403
+ assert @project.closed?
404
+ end
405
+
406
+ test "#open? is inverse of closed?" do
407
+ assert @project.open?
408
+ @project.close!(closed_by: @user)
409
+ assert_not @project.open?
410
+ end
411
+
412
+ test "#reopen! destroys closure record" do
413
+ @project.close!(closed_by: @user)
414
+ @project.reopen!
415
+ assert @project.open?
416
+ assert_nil @project.reload.closure
417
+ end
418
+
419
+ test ".open scope returns unclosed records" do
420
+ open_project = projects(:website_redesign)
421
+ closed_project = projects(:archived_project)
422
+ # archived_project has a closure fixture
423
+
424
+ results = Project.open
425
+ assert_includes results, open_project
426
+ assert_not_includes results, closed_project
427
+ end
428
+
429
+ test ".closed scope returns closed records" do
430
+ @project.close!(closed_by: @user)
431
+ assert_includes Project.closed, @project
432
+ end
433
+ end
434
+ ```
435
+
436
+ ## File Organization
437
+
438
+ ```
439
+ app/
440
+ models/
441
+ concerns/
442
+ closeable.rb # State: open/closed
443
+ publishable.rb # State: draft/published
444
+ searchable.rb # Search capability
445
+ trackable.rb # Audit trail (created_by, updated_by)
446
+ has_uuid.rb # UUID generation
447
+ contactable.rb # Email/phone validation
448
+ sortable.rb # Position ordering
449
+ controllers/
450
+ concerns/
451
+ authentication.rb # Session management
452
+ set_current_account.rb # Account scoping
453
+ paginatable.rb # Pagination helpers
454
+ error_handling.rb # Rescue handlers
455
+ ```
456
+
457
+ ## Anti-Patterns to Avoid
458
+
459
+ 1. **Kitchen-sink concerns** - One concern doing too many unrelated things. Split into focused concerns.
460
+ 2. **Concern dependencies** - Concern A requiring Concern B to be included. Each concern should be self-contained.
461
+ 3. **Single-use concerns** - If only one model uses it, keep it in the model.
462
+ 4. **Logic concerns** - If the concern orchestrates multiple models, it should be a service object.
463
+ 5. **Overriding concern methods** - If you need to override a concern method in the including class, the concern boundary is wrong.
464
+ 6. **Deeply nested concerns** - Concern including another concern. Keep the hierarchy flat.