rubyn-code 0.1.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 (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,257 @@
1
+ # Gems: RuboCop
2
+
3
+ ## Pattern
4
+
5
+ RuboCop enforces consistent Ruby style across the team. Start with a reasonable base configuration, customize to match your project's conventions, and run it in CI to catch violations before code review.
6
+
7
+ ### Setup
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ group :development, :test do
12
+ gem "rubocop", require: false
13
+ gem "rubocop-rails", require: false # Rails-specific cops
14
+ gem "rubocop-rspec", require: false # RSpec-specific cops (if using RSpec)
15
+ gem "rubocop-minitest", require: false # Minitest-specific cops (if using Minitest)
16
+ gem "rubocop-performance", require: false # Performance-focused cops
17
+ gem "rubocop-rails-omakase", require: false # DHH's Rails opinions (optional)
18
+ end
19
+ ```
20
+
21
+ ### Configuration
22
+
23
+ ```yaml
24
+ # .rubocop.yml
25
+ require:
26
+ - rubocop-rails
27
+ - rubocop-rspec # or rubocop-minitest
28
+ - rubocop-performance
29
+
30
+ AllCops:
31
+ TargetRubyVersion: 3.3
32
+ NewCops: enable
33
+ Exclude:
34
+ - "db/schema.rb"
35
+ - "db/migrate/**/*"
36
+ - "bin/**/*"
37
+ - "vendor/**/*"
38
+ - "node_modules/**/*"
39
+ - "tmp/**/*"
40
+
41
+ # ─── Style ──────────────────────────────────────
42
+ Style/FrozenStringLiteralComment:
43
+ Enabled: true
44
+ EnforcedStyle: always
45
+
46
+ Style/StringLiterals:
47
+ EnforcedStyle: double_quotes
48
+
49
+ Style/SymbolArray:
50
+ EnforcedStyle: brackets
51
+
52
+ Style/WordArray:
53
+ EnforcedStyle: brackets
54
+
55
+ Style/Documentation:
56
+ Enabled: false # Don't require class documentation comments
57
+
58
+ Style/ClassAndModuleChildren:
59
+ Enabled: false # Allow both nested and compact styles
60
+
61
+ # ─── Layout ─────────────────────────────────────
62
+ Layout/LineLength:
63
+ Max: 120 # 80 is too aggressive for modern screens
64
+ AllowedPatterns:
65
+ - "^\\s*#" # Don't enforce on comments
66
+ - "https?://" # Don't break URLs
67
+
68
+ Layout/MultilineMethodCallIndentation:
69
+ EnforcedStyle: indented
70
+
71
+ # ─── Metrics ────────────────────────────────────
72
+ Metrics/MethodLength:
73
+ Max: 15 # Default 10 is too strict for real-world Rails
74
+ CountAsOne:
75
+ - array
76
+ - hash
77
+ - heredoc
78
+ Exclude:
79
+ - "db/migrate/**/*"
80
+
81
+ Metrics/ClassLength:
82
+ Max: 200 # Models can be long — extract when it hurts
83
+ Exclude:
84
+ - "app/models/**/*" # Models get a pass — concerns handle extraction
85
+
86
+ Metrics/AbcSize:
87
+ Max: 20 # Default 17 triggers on straightforward methods
88
+
89
+ Metrics/BlockLength:
90
+ Exclude:
91
+ - "spec/**/*" # RSpec blocks are naturally long
92
+ - "test/**/*" # Minitest blocks too
93
+ - "config/routes.rb"
94
+ - "config/environments/**/*"
95
+ - "lib/tasks/**/*"
96
+ - "*.gemspec"
97
+
98
+ # ─── Rails ──────────────────────────────────────
99
+ Rails/HasManyOrHasOneDependent:
100
+ Enabled: true # Force dependent: option on has_many/has_one
101
+
102
+ Rails/InverseOf:
103
+ Enabled: true # Suggest inverse_of on associations
104
+
105
+ Rails/UnknownEnv:
106
+ Environments:
107
+ - production
108
+ - development
109
+ - test
110
+ - staging
111
+
112
+ # ─── RSpec ──────────────────────────────────────
113
+ RSpec/ExampleLength:
114
+ Max: 15 # Default 5 is too strict
115
+ CountAsOne:
116
+ - array
117
+ - hash
118
+ - heredoc
119
+
120
+ RSpec/MultipleExpectations:
121
+ Max: 5 # Allow aggregate_failures style
122
+
123
+ RSpec/NestedGroups:
124
+ Max: 4
125
+
126
+ RSpec/MultipleMemoizedHelpers:
127
+ Max: 8 # Real specs need setup
128
+
129
+ # ─── Performance ────────────────────────────────
130
+ Performance/CollectionLiteralInLoop:
131
+ Enabled: true
132
+
133
+ Performance/Count:
134
+ Enabled: true # Prefer .count over .select { }.size
135
+
136
+ Performance/Detect:
137
+ Enabled: true # Prefer .detect over .select { }.first
138
+ ```
139
+
140
+ ### Running
141
+
142
+ ```bash
143
+ # Check all files
144
+ bundle exec rubocop
145
+
146
+ # Check specific files
147
+ bundle exec rubocop app/models/order.rb
148
+
149
+ # Auto-correct safe violations
150
+ bundle exec rubocop -a
151
+
152
+ # Auto-correct all violations (including unsafe)
153
+ bundle exec rubocop -A
154
+
155
+ # Generate a TODO file for existing violations (adopt incrementally)
156
+ bundle exec rubocop --auto-gen-config
157
+ # Creates .rubocop_todo.yml — inherit from it and fix violations over time
158
+
159
+ # Check only new/modified files (CI optimization)
160
+ bundle exec rubocop --only-changed
161
+ ```
162
+
163
+ ### CI Integration
164
+
165
+ ```yaml
166
+ # .github/workflows/lint.yml
167
+ name: Lint
168
+ on: [push, pull_request]
169
+ jobs:
170
+ rubocop:
171
+ runs-on: ubuntu-latest
172
+ steps:
173
+ - uses: actions/checkout@v4
174
+ - uses: ruby/setup-ruby@v1
175
+ with:
176
+ bundler-cache: true
177
+ - run: bundle exec rubocop --parallel
178
+ ```
179
+
180
+ ### Incremental Adoption for Existing Projects
181
+
182
+ ```bash
183
+ # Step 1: Generate TODO file listing all current violations
184
+ bundle exec rubocop --auto-gen-config --auto-gen-only-exclude
185
+
186
+ # Step 2: Add to .rubocop.yml
187
+ # inherit_from: .rubocop_todo.yml
188
+
189
+ # Step 3: Fix violations gradually
190
+ # Each PR that touches a file fixes its violations
191
+ # Over time, the TODO file shrinks to zero
192
+
193
+ # Step 4: Remove the TODO file when it's empty
194
+ ```
195
+
196
+ ### Custom Cops (Advanced)
197
+
198
+ ```ruby
199
+ # lib/rubocop/cop/custom/no_direct_api_calls.rb
200
+ module RuboCop
201
+ module Cop
202
+ module Custom
203
+ class NoDirectApiCalls < Base
204
+ MSG = "Use a service object or adapter instead of calling external APIs directly in controllers."
205
+
206
+ RESTRICTED_RECEIVERS = %w[Faraday Net::HTTP HTTParty RestClient].freeze
207
+
208
+ def on_send(node)
209
+ return unless in_controller?(node)
210
+ return unless RESTRICTED_RECEIVERS.include?(node.receiver&.const_name)
211
+
212
+ add_offense(node)
213
+ end
214
+
215
+ private
216
+
217
+ def in_controller?(node)
218
+ node.each_ancestor(:class).any? do |klass|
219
+ klass.identifier.source.end_with?("Controller")
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ ```
227
+
228
+ ```yaml
229
+ # .rubocop.yml
230
+ require:
231
+ - ./lib/rubocop/cop/custom/no_direct_api_calls
232
+
233
+ Custom/NoDirectApiCalls:
234
+ Enabled: true
235
+ Include:
236
+ - "app/controllers/**/*"
237
+ ```
238
+
239
+ ## Why This Is Good
240
+
241
+ - **Consistent style without arguments.** The config decides once. Every developer and every PR follows the same style. No more "tabs vs spaces" debates.
242
+ - **Catches real bugs.** `Rails/HasManyOrHasOneDependent` catches missing `dependent:` options. `Performance/Detect` catches `.select { }.first` instead of `.detect`. These are functional improvements, not just style.
243
+ - **Auto-correct saves time.** `rubocop -a` fixes 70-80% of violations automatically. String quote style, trailing whitespace, frozen string literal — all fixed in one command.
244
+ - **Incremental adoption.** `--auto-gen-config` lets you adopt RuboCop on existing projects without fixing 500 violations in one PR. Fix as you go.
245
+ - **CI enforcement.** Violations are caught before code review. Reviewers focus on design and logic, not style.
246
+
247
+ ## When To Apply
248
+
249
+ - **Every Ruby project.** RuboCop is non-negotiable. Install it, configure it, run it in CI.
250
+ - **Day one of a new project.** Start clean — no TODO file needed.
251
+ - **Existing projects.** Use `--auto-gen-config` for gradual adoption. Fix violations file-by-file as you touch them.
252
+
253
+ ## When NOT To Apply
254
+
255
+ - **Don't enforce every cop.** Disable cops that don't match your team's preferences. `Style/Documentation` (requires class comments) is commonly disabled. `Metrics/MethodLength: 10` is too strict for most Rails apps.
256
+ - **Don't auto-correct in bulk on large codebases.** A 500-file auto-correct commit is impossible to review. Fix incrementally.
257
+ - **Don't write custom cops until you have 50+ files.** Standard cops cover 99% of needs. Custom cops are for team-specific architectural rules.
@@ -0,0 +1,360 @@
1
+ # Gem: Sidekiq
2
+
3
+ ## What It Is
4
+
5
+ Sidekiq processes background jobs using Redis-backed queues. It's the standard for async work in Rails — sending emails, processing uploads, calling external APIs, and running scheduled tasks. It uses threads (not processes) for concurrency, so it's memory efficient but requires thread-safe code.
6
+
7
+ ## Setup Done Right
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem 'sidekiq'
12
+
13
+ # config/application.rb
14
+ config.active_job.queue_adapter = :sidekiq
15
+
16
+ # config/initializers/sidekiq.rb
17
+ Sidekiq.configure_server do |config|
18
+ config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
19
+ end
20
+
21
+ Sidekiq.configure_client do |config|
22
+ config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
23
+ end
24
+
25
+ # config/sidekiq.yml
26
+ :concurrency: 10
27
+ :queues:
28
+ - [critical, 3]
29
+ - [default, 2]
30
+ - [low, 1]
31
+ ```
32
+
33
+ ## Gotcha #1: Jobs Must Be Idempotent
34
+
35
+ Sidekiq guarantees "at least once" delivery. Jobs can run multiple times due to network issues, process crashes, or manual retries. If your job isn't idempotent, duplicate execution causes real damage.
36
+
37
+ ```ruby
38
+ # WRONG: Non-idempotent — double execution sends two emails
39
+ class OrderConfirmationJob < ApplicationJob
40
+ queue_as :default
41
+
42
+ def perform(order_id)
43
+ order = Order.find(order_id)
44
+ OrderMailer.confirmation(order).deliver_now
45
+ end
46
+ end
47
+
48
+ # RIGHT: Idempotent — second execution is a no-op
49
+ class OrderConfirmationJob < ApplicationJob
50
+ queue_as :default
51
+
52
+ def perform(order_id)
53
+ order = Order.find(order_id)
54
+ return if order.confirmation_sent_at.present? # Already sent
55
+
56
+ OrderMailer.confirmation(order).deliver_now
57
+ order.update!(confirmation_sent_at: Time.current)
58
+ end
59
+ end
60
+ ```
61
+
62
+ ```ruby
63
+ # WRONG: Non-idempotent — double execution double-charges
64
+ class ChargeJob < ApplicationJob
65
+ def perform(order_id)
66
+ order = Order.find(order_id)
67
+ Stripe::Charge.create(amount: order.total_cents, source: order.payment_token)
68
+ order.update!(paid: true)
69
+ end
70
+ end
71
+
72
+ # RIGHT: Check before charging, use idempotency keys
73
+ class ChargeJob < ApplicationJob
74
+ def perform(order_id)
75
+ order = Order.find(order_id)
76
+ return if order.paid? # Already charged
77
+
78
+ Stripe::Charge.create(
79
+ amount: order.total_cents,
80
+ source: order.payment_token,
81
+ idempotency_key: "order-#{order.id}" # Stripe deduplicates
82
+ )
83
+ order.update!(paid: true, paid_at: Time.current)
84
+ end
85
+ end
86
+ ```
87
+
88
+ **The trap:** The job runs, charges the card, then crashes before `update!(paid: true)`. Sidekiq retries. The card is charged again. Always check state before performing side effects, and use provider-level idempotency keys where available.
89
+
90
+ ## Gotcha #2: Pass IDs, Not Objects
91
+
92
+ Sidekiq serializes arguments to JSON and stores them in Redis. ActiveRecord objects can't be serialized, and even if they could, they'd be stale by the time the job runs.
93
+
94
+ ```ruby
95
+ # WRONG: Passing an ActiveRecord object
96
+ OrderConfirmationJob.perform_later(order)
97
+ # ArgumentError: ActiveRecord objects can't be serialized to JSON
98
+
99
+ # WRONG: Passing a hash of attributes
100
+ OrderConfirmationJob.perform_later(order.attributes)
101
+ # Works but: 30 fields serialized, most unused. Stale data if order changes before job runs.
102
+
103
+ # RIGHT: Pass the ID, load fresh data in the job
104
+ OrderConfirmationJob.perform_later(order.id)
105
+
106
+ # RIGHT: For multiple simple values, pass them directly
107
+ CreditDeductionJob.perform_later(user.id, 5, "AI interaction")
108
+ ```
109
+
110
+ **The trap with ActiveJob:** ActiveJob has GlobalID which CAN serialize AR objects via `perform_later(order)`. But the object is loaded from the database when the job runs. If the record is deleted between enqueue and execution, you get `ActiveJob::DeserializationError`. Passing IDs and using `find_by` with nil handling is more robust.
111
+
112
+ ```ruby
113
+ # SAFER: Handle missing records
114
+ class OrderConfirmationJob < ApplicationJob
115
+ discard_on ActiveRecord::RecordNotFound
116
+
117
+ def perform(order_id)
118
+ order = Order.find(order_id) # Raises if deleted — discard_on handles it
119
+ # ...
120
+ end
121
+ end
122
+ ```
123
+
124
+ ## Gotcha #3: Thread Safety
125
+
126
+ Sidekiq uses threads. Shared mutable state across threads causes race conditions.
127
+
128
+ ```ruby
129
+ # WRONG: Class-level mutable state
130
+ class ImportJob < ApplicationJob
131
+ @@processed_count = 0 # Shared across all threads!
132
+
133
+ def perform(file_path)
134
+ CSV.foreach(file_path) do |row|
135
+ import_row(row)
136
+ @@processed_count += 1 # Race condition: two threads increment simultaneously
137
+ end
138
+ end
139
+ end
140
+
141
+ # WRONG: Mutable instance variables that persist between jobs
142
+ class ApiClient
143
+ def initialize
144
+ @last_response = nil # Sidekiq reuses the instance across jobs
145
+ end
146
+ end
147
+
148
+ # RIGHT: Use local variables or thread-safe structures
149
+ class ImportJob < ApplicationJob
150
+ def perform(file_path)
151
+ count = 0 # Local to this execution
152
+ CSV.foreach(file_path) do |row|
153
+ import_row(row)
154
+ count += 1
155
+ end
156
+ Rails.logger.info("Imported #{count} rows")
157
+ end
158
+ end
159
+
160
+ # RIGHT: Use thread-safe operations for shared counters
161
+ class ImportJob < ApplicationJob
162
+ def perform(file_path)
163
+ # Redis is thread-safe — use it for shared state
164
+ Redis.current.set("import:#{file_path}:status", "processing")
165
+ # ...
166
+ Redis.current.set("import:#{file_path}:status", "complete")
167
+ end
168
+ end
169
+ ```
170
+
171
+ **The trap:** Your job works perfectly in development (single thread). In production with `concurrency: 10`, two threads modify the same data and you get corrupted records, duplicate inserts, or wrong counts.
172
+
173
+ ## Gotcha #4: Transaction + Job Enqueue Timing
174
+
175
+ If you enqueue a job inside a transaction that hasn't committed yet, the job might run before the transaction commits — and the record won't exist yet.
176
+
177
+ ```ruby
178
+ # WRONG: Job fires before transaction commits
179
+ ActiveRecord::Base.transaction do
180
+ order = Order.create!(params)
181
+ OrderConfirmationJob.perform_later(order.id) # Job starts immediately
182
+ # Transaction hasn't committed yet — job might run and Order.find raises!
183
+ update_inventory(order)
184
+ end
185
+
186
+ # RIGHT: Enqueue after the transaction commits
187
+ ActiveRecord::Base.transaction do
188
+ order = Order.create!(params)
189
+ update_inventory(order)
190
+ end
191
+ # Transaction committed — record exists in DB
192
+ OrderConfirmationJob.perform_later(order.id)
193
+
194
+ # RIGHT: Use after_commit callback
195
+ class Order < ApplicationRecord
196
+ after_commit :send_confirmation, on: :create
197
+
198
+ private
199
+
200
+ def send_confirmation
201
+ OrderConfirmationJob.perform_later(id)
202
+ end
203
+ end
204
+ ```
205
+
206
+ ## Gotcha #5: Retry Strategy
207
+
208
+ Sidekiq retries failed jobs with exponential backoff by default (25 retries over ~21 days). This is usually too aggressive.
209
+
210
+ ```ruby
211
+ # Configure retries per job
212
+ class WebhookDeliveryJob < ApplicationJob
213
+ queue_as :webhooks
214
+
215
+ # ActiveJob retry configuration
216
+ retry_on Faraday::TimeoutError, wait: :polynomially_longer, attempts: 5
217
+ retry_on Faraday::ServerError, wait: 30.seconds, attempts: 3
218
+ discard_on Faraday::ClientError # 4xx errors won't succeed on retry
219
+
220
+ # OR: Sidekiq-native retry configuration (use one or the other, not both)
221
+ sidekiq_options retry: 5
222
+
223
+ def perform(webhook_id)
224
+ webhook = Webhook.find(webhook_id)
225
+ response = Faraday.post(webhook.url, webhook.payload.to_json)
226
+ raise Faraday::ServerError, "HTTP #{response.status}" unless response.success?
227
+ end
228
+ end
229
+
230
+ # Jobs that should NEVER retry
231
+ class OneTimeImportJob < ApplicationJob
232
+ sidekiq_options retry: 0 # or: discard_on StandardError
233
+
234
+ def perform(file_path)
235
+ # ...
236
+ end
237
+ end
238
+ ```
239
+
240
+ **The trap:** A job that calls an external API fails. Sidekiq retries 25 times over 21 days. The external API was down for 5 minutes. After it recovers, your job retries with stale data from 3 weeks ago. Set appropriate retry limits and consider discarding jobs that are too old.
241
+
242
+ ## Gotcha #6: Queue Priority Starvation
243
+
244
+ Sidekiq processes queues in the order listed. Without weights, a flood of low-priority jobs can starve critical ones.
245
+
246
+ ```yaml
247
+ # WRONG: No weights — Sidekiq processes strictly in order
248
+ :queues:
249
+ - critical
250
+ - default
251
+ - low
252
+ # If 1000 critical jobs are queued, default and low NEVER run until critical is empty
253
+
254
+ # RIGHT: Weighted queues
255
+ :queues:
256
+ - [critical, 3] # 3x more likely to be picked than low
257
+ - [default, 2] # 2x more likely than low
258
+ - [low, 1] # Baseline priority
259
+ ```
260
+
261
+ ```ruby
262
+ # Assign queues by job type
263
+ class PaymentJob < ApplicationJob
264
+ queue_as :critical
265
+ end
266
+
267
+ class EmailJob < ApplicationJob
268
+ queue_as :default
269
+ end
270
+
271
+ class ReportJob < ApplicationJob
272
+ queue_as :low
273
+ end
274
+ ```
275
+
276
+ ## Gotcha #7: Memory and Large Arguments
277
+
278
+ Sidekiq stores job arguments in Redis. Large arguments consume Redis memory and slow down serialization.
279
+
280
+ ```ruby
281
+ # WRONG: Passing large data through Redis
282
+ class ProcessDataJob < ApplicationJob
283
+ def perform(csv_data) # csv_data could be 50MB
284
+ # This serializes 50MB to JSON, stores in Redis, deserializes in the worker
285
+ end
286
+ end
287
+
288
+ # RIGHT: Pass a reference, load data in the job
289
+ class ProcessDataJob < ApplicationJob
290
+ def perform(file_path)
291
+ data = File.read(file_path) # OR: ActiveStorage download
292
+ process(data)
293
+ end
294
+ end
295
+
296
+ # RIGHT: For S3/ActiveStorage files
297
+ class ProcessUploadJob < ApplicationJob
298
+ def perform(blob_id)
299
+ blob = ActiveStorage::Blob.find(blob_id)
300
+ blob.open do |file|
301
+ process(file)
302
+ end
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Gotcha #8: Testing Sidekiq Jobs
308
+
309
+ ```ruby
310
+ # spec/jobs/order_confirmation_job_spec.rb
311
+ RSpec.describe OrderConfirmationJob, type: :job do
312
+ let(:order) { create(:order) }
313
+
314
+ # Test the job logic directly
315
+ it "sends a confirmation email" do
316
+ expect {
317
+ described_class.perform_now(order.id)
318
+ }.to change { ActionMailer::Base.deliveries.count }.by(1)
319
+ end
320
+
321
+ it "is idempotent" do
322
+ described_class.perform_now(order.id)
323
+ described_class.perform_now(order.id)
324
+ expect(ActionMailer::Base.deliveries.count).to eq(1) # Sent once, not twice
325
+ end
326
+
327
+ it "handles missing records" do
328
+ expect { described_class.perform_now(999_999) }
329
+ .not_to raise_error # discard_on handles it
330
+ end
331
+
332
+ # Test that the job is enqueued
333
+ it "is enqueued after order creation" do
334
+ expect { create(:order) }
335
+ .to have_enqueued_job(described_class)
336
+ .with(kind_of(Integer))
337
+ .on_queue("default")
338
+ end
339
+ end
340
+ ```
341
+
342
+ ## Do's and Don'ts Summary
343
+
344
+ **DO:**
345
+ - Make every job idempotent — check state before acting
346
+ - Pass IDs, not objects or large data
347
+ - Use `discard_on` for errors that won't succeed on retry
348
+ - Set explicit retry counts per job type
349
+ - Use weighted queues to prevent starvation
350
+ - Enqueue jobs after transaction commit
351
+ - Test idempotency explicitly
352
+
353
+ **DON'T:**
354
+ - Don't use class variables or global mutable state in jobs
355
+ - Don't pass large payloads (>100KB) as job arguments
356
+ - Don't enqueue inside transactions without `after_commit`
357
+ - Don't assume jobs run exactly once — they run at least once
358
+ - Don't use the default 25 retries for everything
359
+ - Don't use `perform_now` in production code (defeats the purpose of async)
360
+ - Don't forget to start the Sidekiq process separately (`bundle exec sidekiq`)