agent-notes 2.0.4__py3-none-any.whl

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 (162) hide show
  1. agent_notes/VERSION +1 -0
  2. agent_notes/__init__.py +1 -0
  3. agent_notes/__main__.py +4 -0
  4. agent_notes/cli.py +348 -0
  5. agent_notes/commands/__init__.py +27 -0
  6. agent_notes/commands/_install_helpers.py +262 -0
  7. agent_notes/commands/build.py +170 -0
  8. agent_notes/commands/doctor.py +112 -0
  9. agent_notes/commands/info.py +95 -0
  10. agent_notes/commands/install.py +99 -0
  11. agent_notes/commands/list.py +169 -0
  12. agent_notes/commands/memory.py +430 -0
  13. agent_notes/commands/regenerate.py +152 -0
  14. agent_notes/commands/set_role.py +143 -0
  15. agent_notes/commands/uninstall.py +26 -0
  16. agent_notes/commands/update.py +169 -0
  17. agent_notes/commands/validate.py +199 -0
  18. agent_notes/commands/wizard.py +720 -0
  19. agent_notes/config.py +154 -0
  20. agent_notes/data/agents/agents.yaml +352 -0
  21. agent_notes/data/agents/analyst.md +45 -0
  22. agent_notes/data/agents/api-reviewer.md +47 -0
  23. agent_notes/data/agents/architect.md +46 -0
  24. agent_notes/data/agents/coder.md +28 -0
  25. agent_notes/data/agents/database-specialist.md +45 -0
  26. agent_notes/data/agents/debugger.md +47 -0
  27. agent_notes/data/agents/devil.md +47 -0
  28. agent_notes/data/agents/devops.md +38 -0
  29. agent_notes/data/agents/explorer.md +23 -0
  30. agent_notes/data/agents/integrations.md +44 -0
  31. agent_notes/data/agents/lead.md +216 -0
  32. agent_notes/data/agents/performance-profiler.md +44 -0
  33. agent_notes/data/agents/refactorer.md +48 -0
  34. agent_notes/data/agents/reviewer.md +44 -0
  35. agent_notes/data/agents/security-auditor.md +44 -0
  36. agent_notes/data/agents/system-auditor.md +38 -0
  37. agent_notes/data/agents/tech-writer.md +32 -0
  38. agent_notes/data/agents/test-runner.md +36 -0
  39. agent_notes/data/agents/test-writer.md +39 -0
  40. agent_notes/data/cli/claude.yaml +25 -0
  41. agent_notes/data/cli/copilot.yaml +18 -0
  42. agent_notes/data/cli/opencode.yaml +22 -0
  43. agent_notes/data/commands/brainstorm.md +8 -0
  44. agent_notes/data/commands/debug.md +9 -0
  45. agent_notes/data/commands/review.md +10 -0
  46. agent_notes/data/global-claude.md +290 -0
  47. agent_notes/data/global-copilot.md +27 -0
  48. agent_notes/data/global-opencode.md +40 -0
  49. agent_notes/data/hooks/session-context.md.tpl +19 -0
  50. agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
  51. agent_notes/data/models/claude-opus-4-1.yaml +16 -0
  52. agent_notes/data/models/claude-opus-4-5.yaml +16 -0
  53. agent_notes/data/models/claude-opus-4-6.yaml +16 -0
  54. agent_notes/data/models/claude-opus-4-7.yaml +15 -0
  55. agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
  56. agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
  57. agent_notes/data/models/claude-sonnet-4.yaml +16 -0
  58. agent_notes/data/pricing.yaml +33 -0
  59. agent_notes/data/roles/orchestrator.yaml +5 -0
  60. agent_notes/data/roles/reasoner.yaml +5 -0
  61. agent_notes/data/roles/scout.yaml +5 -0
  62. agent_notes/data/roles/worker.yaml +5 -0
  63. agent_notes/data/rules/code-quality.md +9 -0
  64. agent_notes/data/rules/safety.md +10 -0
  65. agent_notes/data/scripts/cost-report +211 -0
  66. agent_notes/data/skills/brainstorming/SKILL.md +57 -0
  67. agent_notes/data/skills/code-review/SKILL.md +64 -0
  68. agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
  69. agent_notes/data/skills/docker-compose/SKILL.md +318 -0
  70. agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
  71. agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
  72. agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
  73. agent_notes/data/skills/git/SKILL.md +87 -0
  74. agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
  75. agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
  76. agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
  77. agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
  78. agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
  79. agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
  80. agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
  81. agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
  82. agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
  83. agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
  84. agent_notes/data/skills/rails-lib/SKILL.md +101 -0
  85. agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
  86. agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
  87. agent_notes/data/skills/rails-models/SKILL.md +459 -0
  88. agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
  89. agent_notes/data/skills/rails-routes/SKILL.md +804 -0
  90. agent_notes/data/skills/rails-style/SKILL.md +538 -0
  91. agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
  92. agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
  93. agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
  94. agent_notes/data/skills/rails-validations/SKILL.md +108 -0
  95. agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
  96. agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
  97. agent_notes/data/skills/rails-views/SKILL.md +413 -0
  98. agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
  99. agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
  100. agent_notes/data/skills/tdd/SKILL.md +57 -0
  101. agent_notes/data/templates/__init__.py +1 -0
  102. agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  103. agent_notes/data/templates/frontmatter/__init__.py +1 -0
  104. agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  105. agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  106. agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
  107. agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  108. agent_notes/data/templates/frontmatter/claude.py +44 -0
  109. agent_notes/data/templates/frontmatter/opencode.py +104 -0
  110. agent_notes/doctor_checks.py +189 -0
  111. agent_notes/domain/__init__.py +17 -0
  112. agent_notes/domain/agent.py +34 -0
  113. agent_notes/domain/cli_backend.py +40 -0
  114. agent_notes/domain/diagnostics.py +29 -0
  115. agent_notes/domain/diff.py +44 -0
  116. agent_notes/domain/model.py +27 -0
  117. agent_notes/domain/role.py +13 -0
  118. agent_notes/domain/rule.py +13 -0
  119. agent_notes/domain/skill.py +15 -0
  120. agent_notes/domain/state.py +46 -0
  121. agent_notes/install_state.py +11 -0
  122. agent_notes/registries/__init__.py +16 -0
  123. agent_notes/registries/_base.py +46 -0
  124. agent_notes/registries/agent_registry.py +107 -0
  125. agent_notes/registries/cli_registry.py +89 -0
  126. agent_notes/registries/model_registry.py +85 -0
  127. agent_notes/registries/role_registry.py +64 -0
  128. agent_notes/registries/rule_registry.py +80 -0
  129. agent_notes/registries/skill_registry.py +141 -0
  130. agent_notes/services/__init__.py +8 -0
  131. agent_notes/services/diagnostics/__init__.py +47 -0
  132. agent_notes/services/diagnostics/_checks.py +272 -0
  133. agent_notes/services/diagnostics/_display.py +346 -0
  134. agent_notes/services/diagnostics/_fix.py +169 -0
  135. agent_notes/services/diff.py +349 -0
  136. agent_notes/services/fs.py +195 -0
  137. agent_notes/services/install_state_builder.py +210 -0
  138. agent_notes/services/installer.py +293 -0
  139. agent_notes/services/memory_backend.py +155 -0
  140. agent_notes/services/rendering.py +329 -0
  141. agent_notes/services/session_context.py +23 -0
  142. agent_notes/services/settings_writer.py +79 -0
  143. agent_notes/services/state_store.py +249 -0
  144. agent_notes/services/ui.py +419 -0
  145. agent_notes/services/user_config.py +62 -0
  146. agent_notes/services/validation.py +67 -0
  147. agent_notes/state.py +21 -0
  148. agent_notes-2.0.4.dist-info/METADATA +14 -0
  149. agent_notes-2.0.4.dist-info/RECORD +162 -0
  150. agent_notes-2.0.4.dist-info/WHEEL +5 -0
  151. agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
  152. agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
  153. agent_notes-2.0.4.dist-info/top_level.txt +2 -0
  154. tests/conftest.py +20 -0
  155. tests/functional/__init__.py +0 -0
  156. tests/functional/test_build_commands.py +88 -0
  157. tests/functional/test_registries.py +128 -0
  158. tests/integration/__init__.py +0 -0
  159. tests/integration/test_build_output.py +129 -0
  160. tests/plugins/__init__.py +0 -0
  161. tests/plugins/test_agents.py +93 -0
  162. tests/plugins/test_skills.py +77 -0
@@ -0,0 +1,806 @@
1
+ ---
2
+ name: rails-concerns
3
+ description: "Rails concerns: model and controller concern patterns, shared behavior, and testing"
4
+ group: rails
5
+ ---
6
+
7
+ # Concerns
8
+
9
+ Comprehensive patterns and best practices for Rails concerns (both model and controller).
10
+
11
+ ---
12
+
13
+ ## Philosophy
14
+
15
+ **Concerns extract shared or feature-specific behavior into reusable modules.**
16
+
17
+ Two types:
18
+ 1. **Model Concerns** - Feature-specific (`Card::Closeable`) or shared (`Searchable`)
19
+ 2. **Controller Concerns** - Shared behavior (`Authentication`, `CardScoped`)
20
+
21
+ ---
22
+
23
+ ## Model Concerns
24
+
25
+ ### File Structure
26
+
27
+ ```
28
+ app/models/
29
+ ├── card/
30
+ │ ├── closeable.rb # Feature concern (Card::Closeable)
31
+ │ ├── golden.rb # Feature concern
32
+ │ └── pinnable.rb # Feature concern
33
+ └── concerns/
34
+ ├── eventable.rb # Shared concern
35
+ └── searchable.rb # Shared concern
36
+ ```
37
+
38
+ ### Feature Concern Template (Model-Specific)
39
+
40
+ ```ruby
41
+ # app/models/card/closeable.rb
42
+ module Card::Closeable
43
+ extend ActiveSupport::Concern
44
+
45
+ # included block runs when module is included
46
+ included do
47
+ # Associations for this feature
48
+ has_one :closure, dependent: :destroy
49
+
50
+ # Scopes
51
+ scope :closed, -> { joins(:closure) }
52
+ scope :open, -> { where.missing(:closure) }
53
+ scope :recently_closed_first, -> {
54
+ closed.order("closures.created_at": :desc)
55
+ }
56
+
57
+ # Callbacks (if needed for this feature)
58
+ after_update_commit :broadcast_closure_change, if: :saved_change_to_closure?
59
+ end
60
+
61
+ # Class methods (optional)
62
+ class_methods do
63
+ def close_all_stale
64
+ open.where(last_active_at: ..1.month.ago).find_each(&:close)
65
+ end
66
+ end
67
+
68
+ # Instance methods - Query methods
69
+ def closed?
70
+ closure.present?
71
+ end
72
+
73
+ def closed_by
74
+ closure&.user
75
+ end
76
+
77
+ def closed_at
78
+ closure&.created_at
79
+ end
80
+
81
+ # Instance methods - Action methods (use transactions + events)
82
+ def close(user: Current.user)
83
+ return if closed?
84
+
85
+ transaction do
86
+ create_closure! user: user
87
+ track_event :closed, creator: user
88
+ end
89
+ end
90
+
91
+ def reopen(user: Current.user)
92
+ return unless closed?
93
+
94
+ transaction do
95
+ closure.destroy
96
+ track_event :reopened, creator: user
97
+ end
98
+ end
99
+
100
+ # Private methods specific to this concern
101
+ private
102
+ def broadcast_closure_change
103
+ broadcast_refresh_later
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### Shared Concern Template
109
+
110
+ ```ruby
111
+ # app/models/concerns/eventable.rb
112
+ module Eventable
113
+ extend ActiveSupport::Concern
114
+
115
+ included do
116
+ has_many :events, as: :eventable, dependent: :destroy
117
+ end
118
+
119
+ def track_event(action, creator: Current.user, board: self.board, **particulars)
120
+ if should_track_event?
121
+ board.events.create!(
122
+ action: "#{eventable_prefix}_#{action}",
123
+ creator: creator,
124
+ board: board,
125
+ eventable: self,
126
+ particulars: particulars
127
+ )
128
+ end
129
+ end
130
+
131
+ private
132
+ def eventable_prefix
133
+ self.class.name.demodulize.underscore
134
+ end
135
+
136
+ def should_track_event?
137
+ true # Override in including class if needed
138
+ end
139
+ end
140
+ ```
141
+
142
+ ### Production Examples
143
+
144
+ #### Card::Golden
145
+
146
+ ```ruby
147
+ module Card::Golden
148
+ extend ActiveSupport::Concern
149
+
150
+ included do
151
+ has_one :goldness, dependent: :destroy, class_name: "Card::Goldness"
152
+
153
+ scope :golden, -> { joins(:goldness) }
154
+ scope :with_golden_first, -> {
155
+ left_outer_joins(:goldness)
156
+ .prepend_order("card_goldnesses.id IS NULL")
157
+ .preload(:goldness)
158
+ }
159
+ end
160
+
161
+ def golden?
162
+ goldness.present?
163
+ end
164
+
165
+ def gild
166
+ create_goldness! unless golden?
167
+ end
168
+
169
+ def ungild
170
+ goldness&.destroy
171
+ end
172
+ end
173
+ ```
174
+
175
+ #### Card::Pinnable
176
+
177
+ ```ruby
178
+ module Card::Pinnable
179
+ extend ActiveSupport::Concern
180
+
181
+ included do
182
+ has_many :pins, dependent: :destroy
183
+
184
+ after_update_commit :broadcast_pin_updates, if: :preview_changed?
185
+ end
186
+
187
+ def pinned_by?(user)
188
+ pins.exists?(user: user)
189
+ end
190
+
191
+ def pin_for(user)
192
+ pins.find_by(user: user)
193
+ end
194
+
195
+ def pin_by(user)
196
+ pins.find_or_create_by!(user: user)
197
+ end
198
+
199
+ def unpin_by(user)
200
+ pins.find_by(user: user)&.destroy
201
+ end
202
+
203
+ private
204
+ def broadcast_pin_updates
205
+ pins.find_each do |pin|
206
+ pin.broadcast_replace_later_to [ pin.user, :pins_tray ],
207
+ partial: "my/pins/pin"
208
+ end
209
+ end
210
+ end
211
+ ```
212
+
213
+ #### Card::Taggable
214
+
215
+ ```ruby
216
+ module Card::Taggable
217
+ extend ActiveSupport::Concern
218
+
219
+ included do
220
+ has_many :taggings, dependent: :destroy
221
+ has_many :tags, through: :taggings
222
+
223
+ scope :tagged_with, ->(tags) {
224
+ joins(:taggings).where(taggings: { tag: tags })
225
+ }
226
+ end
227
+
228
+ def toggle_tag_with(title)
229
+ tag = account.tags.find_or_create_by!(title: title)
230
+
231
+ transaction do
232
+ if tagged_with?(tag)
233
+ taggings.destroy_by tag: tag
234
+ else
235
+ taggings.create tag: tag
236
+ end
237
+ end
238
+ end
239
+
240
+ def tagged_with?(tag)
241
+ tags.include? tag
242
+ end
243
+ end
244
+ ```
245
+
246
+ #### Searchable (Shared)
247
+
248
+ ```ruby
249
+ module Searchable
250
+ extend ActiveSupport::Concern
251
+
252
+ included do
253
+ after_create_commit :create_in_search_index
254
+ after_update_commit :update_in_search_index
255
+ after_destroy_commit :remove_from_search_index
256
+ end
257
+
258
+ def reindex
259
+ update_in_search_index
260
+ end
261
+
262
+ private
263
+ def create_in_search_index
264
+ search_record_class.create!(search_record_attributes)
265
+ end
266
+
267
+ def update_in_search_index
268
+ search_record_class.upsert!(search_record_attributes)
269
+ end
270
+
271
+ def remove_from_search_index
272
+ search_record_class
273
+ .find_by(searchable_type: self.class.name, searchable_id: id)
274
+ &.destroy
275
+ end
276
+
277
+ def search_record_attributes
278
+ {
279
+ account_id: account_id,
280
+ searchable_type: self.class.name,
281
+ searchable_id: id,
282
+ card_id: search_card_id,
283
+ board_id: search_board_id,
284
+ title: search_title,
285
+ content: search_content,
286
+ created_at: created_at
287
+ }
288
+ end
289
+
290
+ def search_record_class
291
+ Search::Record.for(account_id)
292
+ end
293
+
294
+ # Including models must implement:
295
+ # - search_title
296
+ # - search_content
297
+ # - search_card_id
298
+ # - search_board_id
299
+ end
300
+ ```
301
+
302
+ #### Broadcastable
303
+
304
+ ```ruby
305
+ module Card::Broadcastable
306
+ extend ActiveSupport::Concern
307
+
308
+ included do
309
+ broadcasts_refreshes # Turbo auto-refresh
310
+
311
+ before_update :remember_if_preview_changed
312
+ end
313
+
314
+ private
315
+ def remember_if_preview_changed
316
+ @preview_changed ||= title_changed? || column_id_changed? || board_id_changed?
317
+ end
318
+
319
+ def preview_changed?
320
+ @preview_changed
321
+ end
322
+ end
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Controller Concerns
328
+
329
+ ### File Structure
330
+
331
+ ```
332
+ app/controllers/concerns/
333
+ ├── authentication.rb # Authentication/authorization
334
+ ├── card_scoped.rb # Scoping concern
335
+ ├── board_scoped.rb # Scoping concern
336
+ └── filter_scoped.rb # Filter concern
337
+ ```
338
+
339
+ ### Authentication Concern
340
+
341
+ ```ruby
342
+ # app/controllers/concerns/authentication.rb
343
+ module Authentication
344
+ extend ActiveSupport::Concern
345
+
346
+ included do
347
+ before_action :require_account
348
+ before_action :require_authentication
349
+
350
+ helper_method :authenticated?, :current_user, :current_identity
351
+ end
352
+
353
+ class_methods do
354
+ # Allow unauthenticated access to specific actions
355
+ def allow_unauthenticated_access(**options)
356
+ skip_before_action :require_authentication, **options
357
+ before_action :resume_session, **options
358
+ end
359
+
360
+ # Require unauthenticated (for login/signup pages)
361
+ def require_unauthenticated_access(**options)
362
+ allow_unauthenticated_access **options
363
+ before_action :redirect_authenticated_user, **options
364
+ end
365
+ end
366
+
367
+ private
368
+ def authenticated?
369
+ Current.identity.present?
370
+ end
371
+
372
+ def current_user
373
+ Current.user
374
+ end
375
+
376
+ def current_identity
377
+ Current.identity
378
+ end
379
+
380
+ def require_authentication
381
+ redirect_to new_session_path unless authenticated?
382
+ end
383
+
384
+ def require_account
385
+ unless Current.account
386
+ redirect_to root_url(untenanted: true)
387
+ end
388
+ end
389
+
390
+ def resume_session
391
+ if session_cookie = cookies.signed[:session_id]
392
+ Current.session = Session.find_by(id: session_cookie)
393
+ end
394
+ end
395
+
396
+ def redirect_authenticated_user
397
+ redirect_to root_path if authenticated?
398
+ end
399
+ end
400
+ ```
401
+
402
+ ### Scoping Concern
403
+
404
+ ```ruby
405
+ # app/controllers/concerns/card_scoped.rb
406
+ module CardScoped
407
+ extend ActiveSupport::Concern
408
+
409
+ included do
410
+ before_action :set_card, :set_board
411
+ end
412
+
413
+ private
414
+ def set_card
415
+ @card = Current.user.accessible_cards.find_by!(number: params[:card_id])
416
+ end
417
+
418
+ def set_board
419
+ @board = @card.board
420
+ end
421
+
422
+ # Helper methods for this resource
423
+ def render_card_replacement
424
+ render turbo_stream: turbo_stream.replace(
425
+ [ @card, :card_container ],
426
+ partial: "cards/container",
427
+ method: :morph,
428
+ locals: { card: @card.reload }
429
+ )
430
+ end
431
+
432
+ def render_card_preview_replacement
433
+ render turbo_stream: turbo_stream.replace(
434
+ [ @card, :preview ],
435
+ partial: "cards/display/preview",
436
+ locals: { card: @card.reload }
437
+ )
438
+ end
439
+ end
440
+ ```
441
+
442
+ ### Feature Toggle Concern
443
+
444
+ ```ruby
445
+ module FeatureGuarded
446
+ extend ActiveSupport::Concern
447
+
448
+ included do
449
+ before_action :ensure_feature_enabled
450
+ end
451
+
452
+ private
453
+ def ensure_feature_enabled
454
+ feature_name = controller_name.singularize
455
+
456
+ unless Current.account.feature_enabled?(feature_name)
457
+ respond_to do |format|
458
+ format.html { redirect_to root_path, alert: "Feature not available" }
459
+ format.json { head :forbidden }
460
+ end
461
+ end
462
+ end
463
+ end
464
+ ```
465
+
466
+ ### Pagination Concern
467
+
468
+ ```ruby
469
+ module Paginatable
470
+ extend ActiveSupport::Concern
471
+
472
+ private
473
+ def paginate(collection, per_page: 25)
474
+ page = params[:page].to_i.clamp(1..)
475
+ offset = (page - 1) * per_page
476
+
477
+ collection.limit(per_page).offset(offset)
478
+ end
479
+
480
+ def pagination_meta(collection, per_page: 25)
481
+ total = collection.count
482
+ page = params[:page].to_i.clamp(1..)
483
+
484
+ {
485
+ current_page: page,
486
+ per_page: per_page,
487
+ total_pages: (total.to_f / per_page).ceil,
488
+ total_count: total
489
+ }
490
+ end
491
+ end
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Job Concerns
497
+
498
+ ### SMTP Error Handling
499
+
500
+ ```ruby
501
+ # app/jobs/concerns/smtp_delivery_error_handling.rb
502
+ module SmtpDeliveryErrorHandling
503
+ extend ActiveSupport::Concern
504
+
505
+ included do
506
+ # Retry delivery to possibly-unavailable remote mailservers
507
+ retry_on Net::OpenTimeout, Net::ReadTimeout, Socket::ResolutionError,
508
+ wait: :polynomially_longer
509
+
510
+ # Net::SMTPServerBusy is SMTP error code 4xx (temporary error)
511
+ retry_on Net::SMTPServerBusy, wait: :polynomially_longer
512
+
513
+ # SMTP syntax errors (50x)
514
+ rescue_from Net::SMTPSyntaxError do |error|
515
+ case error.message
516
+ when /\A501 5\.1\.3/
517
+ # Ignore undeliverable email addresses, but log for monitoring
518
+ Sentry.capture_exception error, level: :info
519
+ else
520
+ raise
521
+ end
522
+ end
523
+
524
+ # SMTP fatal errors (5xx)
525
+ rescue_from Net::SMTPFatalError do |error|
526
+ case error.message
527
+ when /\A550 5\.1\.1/, /\A552 5\.6\.0/, /\A555 5\.5\.4/
528
+ Sentry.capture_exception error, level: :info
529
+ else
530
+ raise
531
+ end
532
+ end
533
+ end
534
+ end
535
+ ```
536
+
537
+ ---
538
+
539
+ ## Concern Patterns
540
+
541
+ ### Dependency Injection
542
+
543
+ ```ruby
544
+ module Notifiable
545
+ extend ActiveSupport::Concern
546
+
547
+ # Including class must implement notify_users method
548
+ def send_notifications
549
+ users_to_notify.each do |user|
550
+ notify_user(user)
551
+ end
552
+ end
553
+
554
+ private
555
+ def users_to_notify
556
+ raise NotImplementedError, "Include class must implement users_to_notify"
557
+ end
558
+
559
+ def notify_user(user)
560
+ raise NotImplementedError, "Include class must implement notify_user"
561
+ end
562
+ end
563
+ ```
564
+
565
+ ### Configuration
566
+
567
+ ```ruby
568
+ module Configurable
569
+ extend ActiveSupport::Concern
570
+
571
+ included do
572
+ class_attribute :config, default: {}
573
+ end
574
+
575
+ class_methods do
576
+ def configure(&block)
577
+ self.config = config.dup
578
+ yield config
579
+ end
580
+ end
581
+ end
582
+
583
+ # Usage:
584
+ class Card < ApplicationRecord
585
+ include Configurable
586
+
587
+ configure do |config|
588
+ config[:auto_close_after] = 30.days
589
+ config[:max_attachments] = 10
590
+ end
591
+ end
592
+ ```
593
+
594
+ ### State Machine (Simple)
595
+
596
+ ```ruby
597
+ module Stateful
598
+ extend ActiveSupport::Concern
599
+
600
+ included do
601
+ validates :state, inclusion: { in: states }
602
+ end
603
+
604
+ class_methods do
605
+ def states
606
+ @states ||= []
607
+ end
608
+
609
+ def state(name, &block)
610
+ states << name.to_s
611
+
612
+ define_method("#{name}?") do
613
+ state == name.to_s
614
+ end
615
+
616
+ define_method("#{name}!") do
617
+ update!(state: name.to_s)
618
+ end
619
+ end
620
+ end
621
+ end
622
+
623
+ # Usage:
624
+ class Order < ApplicationRecord
625
+ include Stateful
626
+
627
+ state :pending
628
+ state :processing
629
+ state :shipped
630
+ state :delivered
631
+ end
632
+
633
+ order.pending? # => true
634
+ order.processing! # => updates state to processing
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Best Practices
640
+
641
+ ### ✅ DO
642
+
643
+ 1. **Extract features to concerns**
644
+ ```ruby
645
+ # app/models/card.rb
646
+ include Closeable, Pinnable, Taggable
647
+ ```
648
+
649
+ 2. **Use namespacing for model-specific concerns**
650
+ ```ruby
651
+ # app/models/card/closeable.rb
652
+ module Card::Closeable
653
+ ```
654
+
655
+ 3. **Keep concerns focused**
656
+ - One responsibility per concern
657
+ - Clear, descriptive names
658
+
659
+ 4. **Use class_methods block**
660
+ ```ruby
661
+ class_methods do
662
+ def find_stale
663
+ # ...
664
+ end
665
+ end
666
+ ```
667
+
668
+ 5. **Document required methods**
669
+ ```ruby
670
+ # Including class must implement:
671
+ # - search_title
672
+ # - search_content
673
+ ```
674
+
675
+ 6. **Use transactions in action methods**
676
+ ```ruby
677
+ def close
678
+ transaction do
679
+ create_closure!
680
+ track_event :closed
681
+ end
682
+ end
683
+ ```
684
+
685
+ 7. **Test concerns independently**
686
+
687
+ ### ❌ DON'T
688
+
689
+ 1. **God concerns** - Keep focused
690
+ 2. **Complex dependencies** - Keep concerns independent
691
+ 3. **Deep nesting** - Avoid concerns including concerns
692
+ 4. **Mixing responsibilities** - One concern = one feature
693
+ 5. **Skipping documentation** - Document required methods
694
+
695
+ ---
696
+
697
+ ## Testing Concerns
698
+
699
+ ### Model Concern Test
700
+
701
+ ```ruby
702
+ # test/models/card/closeable_test.rb
703
+ class Card::CloseableTest < ActiveSupport::TestCase
704
+ setup do
705
+ Current.session = sessions(:david)
706
+ end
707
+
708
+ test "close creates closure" do
709
+ card = cards(:logo)
710
+
711
+ assert_not card.closed?
712
+
713
+ assert_difference -> { Card::Closure.count }, +1 do
714
+ card.close(user: users(:kevin))
715
+ end
716
+
717
+ assert card.closed?
718
+ assert_equal users(:kevin), card.closed_by
719
+ end
720
+
721
+ test "reopen removes closure" do
722
+ card = cards(:logo)
723
+ card.close
724
+
725
+ assert card.closed?
726
+
727
+ assert_difference -> { Card::Closure.count }, -1 do
728
+ card.reopen
729
+ end
730
+
731
+ assert_not card.closed?
732
+ end
733
+
734
+ test "closed scope returns closed cards" do
735
+ card = cards(:logo)
736
+ card.close
737
+
738
+ assert_includes Card.closed, card
739
+ assert_not_includes Card.open, card
740
+ end
741
+ end
742
+ ```
743
+
744
+ ### Controller Concern Test
745
+
746
+ ```ruby
747
+ # test/controllers/concerns/authentication_test.rb
748
+ class AuthenticationTest < ActionDispatch::IntegrationTest
749
+ class TestController < ApplicationController
750
+ include Authentication
751
+
752
+ def index
753
+ head :ok
754
+ end
755
+
756
+ def public_action
757
+ head :ok
758
+ end
759
+
760
+ allow_unauthenticated_access only: :public_action
761
+ end
762
+
763
+ setup do
764
+ Rails.application.routes.draw do
765
+ get "test/index" => "authentication_test/test#index"
766
+ get "test/public_action" => "authentication_test/test#public_action"
767
+ end
768
+ end
769
+
770
+ teardown do
771
+ Rails.application.reload_routes!
772
+ end
773
+
774
+ test "requires authentication for protected actions" do
775
+ get "/test/index"
776
+
777
+ assert_redirected_to new_session_path
778
+ end
779
+
780
+ test "allows unauthenticated access to public actions" do
781
+ get "/test/public_action"
782
+
783
+ assert_response :success
784
+ end
785
+
786
+ test "authenticated user can access protected actions" do
787
+ sign_in_as :david
788
+
789
+ get "/test/index"
790
+
791
+ assert_response :success
792
+ end
793
+ end
794
+ ```
795
+
796
+ ---
797
+
798
+ ## Summary
799
+
800
+ - **Model Concerns**: Extract features (`Card::Closeable`) or shared behavior (`Searchable`)
801
+ - **Controller Concerns**: Authentication, scoping, shared actions
802
+ - **Structure**: `included do`, `class_methods`, instance methods, private methods
803
+ - **Focused**: One responsibility per concern
804
+ - **Documented**: Document required methods for including classes
805
+ - **Tested**: Test concerns independently and in context
806
+ - **Organized**: Model-specific in `model/`, shared in `concerns/`