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,212 @@
1
+ # Rails: API Design
2
+
3
+ ## Pattern
4
+
5
+ Design APIs to be consistent, versioned, and self-documenting. Use Grape or Rails API mode with `jbuilder`/`jsonapi-serializer`. Follow RESTful conventions. Handle errors uniformly.
6
+
7
+ ### API Controller Structure
8
+
9
+ ```ruby
10
+ # app/controllers/api/v1/base_controller.rb
11
+ module Api
12
+ module V1
13
+ class BaseController < ActionController::API
14
+ include Authenticatable
15
+ include Paginatable
16
+ include ErrorHandling
17
+
18
+ before_action :authenticate!
19
+
20
+ private
21
+
22
+ def render_success(data, status: :ok, meta: {})
23
+ response = { data: data }
24
+ response[:meta] = meta if meta.present?
25
+ render json: response, status: status
26
+ end
27
+
28
+ def render_created(data)
29
+ render_success(data, status: :created)
30
+ end
31
+
32
+ def render_error(message, status:, details: nil)
33
+ body = { error: { message: message } }
34
+ body[:error][:details] = details if details
35
+ render json: body, status: status
36
+ end
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ ```ruby
43
+ # app/controllers/api/v1/orders_controller.rb
44
+ module Api
45
+ module V1
46
+ class OrdersController < BaseController
47
+ def index
48
+ orders = paginate(current_user.orders.includes(:line_items).recent)
49
+
50
+ render_success(
51
+ orders.map { |o| OrderSerializer.new(o).as_json },
52
+ meta: pagination_meta(orders)
53
+ )
54
+ end
55
+
56
+ def show
57
+ order = current_user.orders.find(params[:id])
58
+ render_success(OrderSerializer.new(order).as_json)
59
+ end
60
+
61
+ def create
62
+ result = Orders::CreateService.call(order_params.to_h, current_user)
63
+
64
+ if result.success?
65
+ render_created(OrderSerializer.new(result.order).as_json)
66
+ else
67
+ render_error("Validation failed", status: :unprocessable_entity,
68
+ details: result.order.errors.full_messages)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def order_params
75
+ params.require(:order).permit(:shipping_address, line_items: [:product_id, :quantity])
76
+ end
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### Consistent Error Responses
83
+
84
+ ```ruby
85
+ # app/controllers/concerns/error_handling.rb
86
+ module ErrorHandling
87
+ extend ActiveSupport::Concern
88
+
89
+ included do
90
+ rescue_from ActiveRecord::RecordNotFound do |e|
91
+ render_error("Resource not found", status: :not_found)
92
+ end
93
+
94
+ rescue_from ActiveRecord::RecordInvalid do |e|
95
+ render_error("Validation failed", status: :unprocessable_entity,
96
+ details: e.record.errors.full_messages)
97
+ end
98
+
99
+ rescue_from ActionController::ParameterMissing do |e|
100
+ render_error("Missing parameter: #{e.param}", status: :bad_request)
101
+ end
102
+
103
+ rescue_from Pundit::NotAuthorizedError do
104
+ render_error("Forbidden", status: :forbidden)
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Serializers
111
+
112
+ ```ruby
113
+ # app/serializers/order_serializer.rb
114
+ class OrderSerializer
115
+ def initialize(order)
116
+ @order = order
117
+ end
118
+
119
+ def as_json(*)
120
+ {
121
+ id: @order.id,
122
+ reference: @order.reference,
123
+ status: @order.status,
124
+ total: @order.total,
125
+ shipping_address: @order.shipping_address,
126
+ line_items: @order.line_items.map { |li| LineItemSerializer.new(li).as_json },
127
+ created_at: @order.created_at.iso8601,
128
+ updated_at: @order.updated_at.iso8601
129
+ }
130
+ end
131
+ end
132
+ ```
133
+
134
+ ### Versioning
135
+
136
+ ```ruby
137
+ # config/routes.rb
138
+ Rails.application.routes.draw do
139
+ namespace :api do
140
+ namespace :v1 do
141
+ resources :orders, only: [:index, :show, :create, :update, :destroy]
142
+ resources :projects, only: [:index, :show, :create] do
143
+ resources :embeddings, only: [:index, :create], controller: "project_embeddings"
144
+ end
145
+ namespace :ai do
146
+ post :refactor
147
+ post :review
148
+ post :explain
149
+ end
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### Authentication
156
+
157
+ ```ruby
158
+ # app/controllers/concerns/authenticatable.rb
159
+ module Authenticatable
160
+ extend ActiveSupport::Concern
161
+
162
+ private
163
+
164
+ def authenticate!
165
+ token = request.headers["Authorization"]&.delete_prefix("Bearer ")
166
+ render_error("Unauthorized", status: :unauthorized) and return unless token
167
+
168
+ api_key = ApiKey.active.find_by_token(token)
169
+ render_error("Invalid API key", status: :unauthorized) and return unless api_key
170
+
171
+ api_key.touch(:last_used_at)
172
+ @current_user = api_key.user
173
+ end
174
+
175
+ def current_user
176
+ @current_user
177
+ end
178
+ end
179
+ ```
180
+
181
+ ## Why This Is Good
182
+
183
+ - **Consistent response shape.** Every success returns `{ data: ... }`. Every error returns `{ error: { message: ..., details: ... } }`. Clients parse responses predictably.
184
+ - **Versioned from day one.** `/api/v1/` allows breaking changes in v2 without breaking existing clients.
185
+ - **Centralized error handling.** `rescue_from` in a concern handles all common exceptions. No begin/rescue in every action.
186
+ - **Serializers control the API surface.** Only expose the fields you intend. Internal fields (password_digest, internal notes) never leak.
187
+ - **Pagination metadata in every list endpoint.** Clients always know total count, current page, and total pages.
188
+
189
+ ## Anti-Pattern
190
+
191
+ Inconsistent responses, no versioning, and exposing model internals:
192
+
193
+ ```ruby
194
+ # BAD: render model directly
195
+ def show
196
+ render json: Order.find(params[:id])
197
+ # Exposes EVERY column including internal fields
198
+ end
199
+
200
+ # BAD: inconsistent error formats
201
+ def create
202
+ order = Order.create!(params)
203
+ render json: order
204
+ rescue => e
205
+ render json: { msg: e.message }, status: 500 # Different shape than other errors
206
+ end
207
+ ```
208
+
209
+ ## When To Apply
210
+
211
+ - **Every API.** Consistent structure, versioning, and error handling should be established in the first endpoint.
212
+ - **Even internal APIs.** Microservice-to-microservice APIs benefit from the same discipline. Future developers will thank you.
@@ -0,0 +1,182 @@
1
+ # Rails: ActiveRecord Associations
2
+
3
+ ## Pattern
4
+
5
+ Define associations explicitly with appropriate options. Always set `dependent` on `has_many`. Use `inverse_of` when Rails can't infer it. Prefer `has_many :through` over `has_and_belongs_to_many`. Use `counter_cache` to avoid N+1 counts.
6
+
7
+ ```ruby
8
+ class User < ApplicationRecord
9
+ has_many :orders, dependent: :destroy, inverse_of: :user
10
+ has_many :line_items, through: :orders
11
+ has_many :reviews, dependent: :destroy
12
+ has_many :project_memberships, dependent: :destroy
13
+ has_many :projects, through: :project_memberships
14
+ has_one :profile, dependent: :destroy
15
+
16
+ # Optional belongs_to (Rails 5+ requires belongs_to by default)
17
+ belongs_to :company, optional: true
18
+ end
19
+
20
+ class Order < ApplicationRecord
21
+ belongs_to :user, counter_cache: true
22
+ has_many :line_items, dependent: :destroy, inverse_of: :order
23
+ has_one :shipment, dependent: :destroy
24
+
25
+ # Scoped association
26
+ has_many :active_line_items, -> { where(cancelled: false) },
27
+ class_name: "LineItem",
28
+ inverse_of: :order
29
+ end
30
+
31
+ class LineItem < ApplicationRecord
32
+ belongs_to :order, counter_cache: true
33
+ belongs_to :product
34
+
35
+ # Validate presence of the association, not just the foreign key
36
+ validates :order, presence: true
37
+ validates :product, presence: true
38
+ end
39
+ ```
40
+
41
+ `has_many :through` for many-to-many with join model:
42
+
43
+ ```ruby
44
+ class Project < ApplicationRecord
45
+ has_many :project_memberships, dependent: :destroy
46
+ has_many :users, through: :project_memberships
47
+ end
48
+
49
+ class ProjectMembership < ApplicationRecord
50
+ belongs_to :project
51
+ belongs_to :user
52
+
53
+ enum :role, { owner: 0, admin: 1, member: 2, viewer: 3 }
54
+
55
+ validates :project_id, uniqueness: { scope: :user_id }
56
+ end
57
+
58
+ class User < ApplicationRecord
59
+ has_many :project_memberships, dependent: :destroy
60
+ has_many :projects, through: :project_memberships
61
+
62
+ def role_in(project)
63
+ project_memberships.find_by(project: project)&.role
64
+ end
65
+
66
+ def member_of?(project)
67
+ project_memberships.exists?(project: project)
68
+ end
69
+ end
70
+ ```
71
+
72
+ ## Why This Is Good
73
+
74
+ - **`dependent: :destroy` prevents orphans.** When a user is deleted, their orders are destroyed too. Without this, you get orphaned records with foreign keys pointing to nothing.
75
+ - **`inverse_of` optimizes memory.** Rails reuses the same object in memory instead of loading a new one. `order.user` and `user.orders.first.user` return the same object instance, saving queries and preventing stale data.
76
+ - **`counter_cache` eliminates count queries.** `user.orders_count` reads a column instead of running `SELECT COUNT(*)`. For pages that display counts for many records, this prevents N+1 count queries.
77
+ - **`has_many :through` gives you a join model.** The join model can have its own attributes (role, permissions, created_at), validations, and callbacks. `has_and_belongs_to_many` can't.
78
+ - **Scoped associations provide named, preloadable subsets.** `order.active_line_items` is preloadable with `includes(:active_line_items)` and reads clearly.
79
+
80
+ ## Anti-Pattern
81
+
82
+ Missing dependent options, using HABTM, and ignoring inverse_of:
83
+
84
+ ```ruby
85
+ class User < ApplicationRecord
86
+ # BAD: No dependent — deleting a user orphans all their orders
87
+ has_many :orders
88
+
89
+ # BAD: HABTM — no join model, can't add attributes or validations
90
+ has_and_belongs_to_many :projects
91
+ end
92
+
93
+ class Order < ApplicationRecord
94
+ # BAD: belongs_to without counter_cache when counts are displayed frequently
95
+ belongs_to :user
96
+
97
+ # BAD: No dependent — deleting an order orphans line items
98
+ has_many :line_items
99
+
100
+ # BAD: Accessing association in a way that breaks inverse_of
101
+ has_many :items, class_name: "LineItem", foreign_key: "order_id"
102
+ # Rails can't infer inverse_of for :items because the name doesn't match
103
+ end
104
+ ```
105
+
106
+ ## Why This Is Bad
107
+
108
+ - **Missing `dependent` creates orphaned records.** `User.destroy` leaves behind orders, line items, and shipments with `user_id` pointing to a deleted record. Foreign key constraints fail or, worse, the data silently rots.
109
+ - **HABTM can't have join attributes.** You can't store when a user joined a project, what role they have, or who invited them. You're stuck with just the two foreign keys. Every non-trivial many-to-many needs a join model eventually — start with `has_many :through`.
110
+ - **Missing `inverse_of` causes extra queries.** Without it, `order.line_items.first.order` loads the order again from the database instead of reusing the object already in memory. In loops, this multiplies into hundreds of unnecessary queries.
111
+ - **Missing `counter_cache` on frequently counted associations.** If your UI shows "12 orders" next to every user, that's a COUNT query per user. With 50 users on the page, that's 50 COUNT queries.
112
+
113
+ ## When To Apply
114
+
115
+ - **Always set `dependent` on `has_many` and `has_one`.** Choose:
116
+ - `:destroy` — run callbacks on each child (use when children have their own dependents or callbacks)
117
+ - `:delete_all` — single DELETE SQL, skip callbacks (faster, use when children have no dependents)
118
+ - `:nullify` — set foreign key to NULL (use when the child can exist without the parent)
119
+ - `:restrict_with_error` — prevent deletion if children exist (use for referential integrity)
120
+
121
+ - **Always use `has_many :through`** for many-to-many. Even if you don't need join attributes today, you will tomorrow.
122
+
123
+ - **Set `inverse_of`** when the association name doesn't match the class name, or when using `:foreign_key`, `:class_name`, or scoped associations.
124
+
125
+ - **Use `counter_cache`** when you display counts in lists (index pages, admin panels, dashboards).
126
+
127
+ ## When NOT To Apply
128
+
129
+ - **Don't add `dependent: :destroy` on `belongs_to`.** Destroying a line item should not destroy the order it belongs to. Dependent options go on the "parent" side (`has_many`/`has_one`).
130
+ - **Don't over-use `counter_cache`.** It adds a write on every insert/delete of the child. If counts are only viewed in admin reports (not on every page load), a query is fine.
131
+ - **Don't create associations you don't need.** If `User` never needs to directly access `LineItem` without going through `Order`, don't add `has_many :line_items, through: :orders` unless you have a concrete use case.
132
+
133
+ ## Edge Cases
134
+
135
+ **Polymorphic associations:**
136
+ Use when multiple models can be the parent:
137
+
138
+ ```ruby
139
+ class Comment < ApplicationRecord
140
+ belongs_to :commentable, polymorphic: true
141
+ end
142
+
143
+ class Order < ApplicationRecord
144
+ has_many :comments, as: :commentable, dependent: :destroy
145
+ end
146
+
147
+ class Product < ApplicationRecord
148
+ has_many :comments, as: :commentable, dependent: :destroy
149
+ end
150
+ ```
151
+
152
+ Downside: polymorphic foreign keys can't have database-level foreign key constraints. Use application-level validations.
153
+
154
+ **Self-referential associations:**
155
+
156
+ ```ruby
157
+ class Employee < ApplicationRecord
158
+ belongs_to :manager, class_name: "Employee", optional: true, inverse_of: :direct_reports
159
+ has_many :direct_reports, class_name: "Employee", foreign_key: :manager_id,
160
+ dependent: :nullify, inverse_of: :manager
161
+ end
162
+ ```
163
+
164
+ **`touch: true` for cache invalidation:**
165
+
166
+ ```ruby
167
+ class LineItem < ApplicationRecord
168
+ belongs_to :order, touch: true # Updates order.updated_at when line item changes
169
+ end
170
+ ```
171
+
172
+ This is essential for Russian doll caching — changing a line item invalidates the order's cache fragment automatically.
173
+
174
+ **Preloading polymorphic associations:**
175
+
176
+ ```ruby
177
+ # Must specify each possible type
178
+ Comment.includes(:commentable) # Works but may generate N queries for N types
179
+
180
+ # Better: preload specific types
181
+ comments = Comment.where(commentable_type: "Order").includes(:commentable)
182
+ ```
@@ -0,0 +1,212 @@
1
+ # Rails: Background Jobs (Sidekiq)
2
+
3
+ ## Pattern
4
+
5
+ Design jobs to be small, idempotent, and retriable. Pass IDs not objects. Set appropriate queues and retry strategies. Use Sidekiq's features (bulk, batches, rate limiting) for complex workflows.
6
+
7
+ ```ruby
8
+ # GOOD: Small, idempotent, passes ID
9
+ class OrderConfirmationJob < ApplicationJob
10
+ queue_as :default
11
+ retry_on ActiveRecord::RecordNotFound, wait: 5.seconds, attempts: 3
12
+
13
+ def perform(order_id)
14
+ order = Order.find(order_id)
15
+ return if order.confirmation_sent? # Idempotent check
16
+
17
+ OrderMailer.confirmation(order).deliver_now
18
+ order.update!(confirmation_sent_at: Time.current)
19
+ end
20
+ end
21
+
22
+ # Enqueue
23
+ OrderConfirmationJob.perform_later(order.id)
24
+ ```
25
+
26
+ ```ruby
27
+ # GOOD: Batch processing with find_each
28
+ class RecalculateTotalsJob < ApplicationJob
29
+ queue_as :low
30
+
31
+ def perform
32
+ Order.where(total: nil).find_each(batch_size: 500) do |order|
33
+ order.update!(total: order.line_items.sum("quantity * unit_price"))
34
+ end
35
+ end
36
+ end
37
+ ```
38
+
39
+ ```ruby
40
+ # GOOD: Job with error handling and dead letter
41
+ class WebhookDeliveryJob < ApplicationJob
42
+ queue_as :webhooks
43
+ retry_on Faraday::TimeoutError, wait: :polynomially_longer, attempts: 5
44
+ discard_on Faraday::ClientError # 4xx errors won't succeed on retry
45
+
46
+ def perform(webhook_id)
47
+ webhook = Webhook.find(webhook_id)
48
+ response = Faraday.post(webhook.url, webhook.payload.to_json, "Content-Type" => "application/json")
49
+
50
+ if response.success?
51
+ webhook.update!(delivered_at: Time.current, status: :delivered)
52
+ else
53
+ webhook.update!(status: :failed, last_error: "HTTP #{response.status}")
54
+ raise Faraday::ServerError, "Webhook failed: #{response.status}"
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ Queue configuration:
61
+
62
+ ```yaml
63
+ # config/sidekiq.yml
64
+ :concurrency: 10
65
+ :queues:
66
+ - [critical, 3] # Payments, auth — 3x priority
67
+ - [default, 2] # Email, notifications — 2x priority
68
+ - [embeddings, 1] # Codebase indexing — normal priority
69
+ - [low, 1] # Reports, cleanup — normal priority
70
+ ```
71
+
72
+ ## Why This Is Good
73
+
74
+ - **Pass IDs, not objects.** Serializing an ActiveRecord object into Redis is fragile — the object might change between enqueue and execution. `Order.find(order_id)` always loads the current state.
75
+ - **Idempotent jobs are safe to retry.** If the job runs twice (Redis failover, process crash, manual retry), `return if order.confirmation_sent?` prevents sending a duplicate email. The second run is a no-op.
76
+ - **`retry_on` with specific exceptions.** Transient errors (timeout, record not found due to replication lag) get retried with backoff. Permanent errors (`discard_on` for client errors) don't waste retries.
77
+ - **Queue separation by priority.** Payment processing gets 3x the scheduling weight of report generation. A backlog of reports doesn't delay payment confirmations.
78
+ - **`find_each` in batch jobs.** Processing 100,000 orders loads 500 at a time, not all at once. Memory stays flat.
79
+
80
+ ## Anti-Pattern
81
+
82
+ Passing objects, doing too much in one job, no idempotency, no retry strategy:
83
+
84
+ ```ruby
85
+ # BAD: Passes entire object
86
+ class ProcessOrderJob < ApplicationJob
87
+ def perform(order)
88
+ # order is a serialized/deserialized AR object — stale data
89
+ order.line_items.each do |item|
90
+ item.product.decrement!(:stock, item.quantity)
91
+ end
92
+ OrderMailer.confirmation(order).deliver_now
93
+ WarehouseApi.notify(order)
94
+ Analytics.track("order_created", order.attributes)
95
+ order.update!(processed: true)
96
+ end
97
+ end
98
+ ```
99
+
100
+ ```ruby
101
+ # BAD: God job that does everything
102
+ class NightlyProcessingJob < ApplicationJob
103
+ def perform
104
+ # Recalculate all totals
105
+ Order.find_each { |o| o.recalculate! }
106
+ # Send reminder emails
107
+ User.inactive.each { |u| ReminderMailer.nudge(u).deliver_now }
108
+ # Clean up old records
109
+ Order.where("created_at < ?", 1.year.ago).destroy_all
110
+ # Generate reports
111
+ ReportGenerator.monthly.generate!
112
+ # Sync to external system
113
+ ExternalSync.full_sync!
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## Why This Is Bad
119
+
120
+ - **Serialized objects are stale.** The order's data at enqueue time may differ from the database when the job runs (seconds, minutes, or hours later). The price could change, the status could update, line items could be modified.
121
+ - **No idempotency.** If `ProcessOrderJob` runs twice, stock is decremented twice, two confirmation emails are sent, and the warehouse is notified twice. Retries after a crash corrupt data.
122
+ - **God jobs can't be retried partially.** If `NightlyProcessingJob` fails during report generation, retrying it re-runs total recalculation, re-sends reminder emails, and re-deletes old records — all of which already completed.
123
+ - **No error isolation.** One failure in the god job kills the entire run. A network error in `ExternalSync.full_sync!` means reports don't generate and reminders don't send.
124
+ - **No queue differentiation.** Everything runs in the default queue. A burst of slow external API calls blocks email delivery.
125
+ - **`deliver_now` in a job.** Mailer delivery should use `deliver_now` inside a job (it's already async). But if the job itself fails and retries, the email sends again — unless you add an idempotency check.
126
+
127
+ ## When To Apply
128
+
129
+ Move work to a background job when ANY of these are true:
130
+
131
+ - **External API calls** — HTTP requests to payment providers, notification services, webhooks. These are slow, unreliable, and shouldn't block a web response.
132
+ - **Email delivery** — Always `deliver_later`, never `deliver_now` in a controller. Let the job handle retries.
133
+ - **Data processing** — Recalculations, imports, exports, reports. These can take seconds or minutes and shouldn't tie up a web worker.
134
+ - **User-facing response doesn't need the result.** If the user doesn't need to see the outcome immediately (like "your report is being generated"), do it in a background job.
135
+
136
+ ## When NOT To Apply
137
+
138
+ - **Don't background everything.** A 50ms database write that the user needs to see the result of (creating a comment, updating a profile) should happen synchronously in the request. Adding a job adds latency (Redis round trip + queue wait) for no benefit.
139
+ - **Don't use jobs for request-response patterns.** If the user is waiting for a result (like a refactored code response), use streaming — not "enqueue a job and poll for completion."
140
+ - **Don't create jobs for operations that must be transactional with the web request.** If creating an order and deducting credits must succeed or fail together, do both in the request within a transaction.
141
+
142
+ ## Edge Cases
143
+
144
+ **Job needs to run after a transaction commits:**
145
+ Use `after_commit` or `ActiveRecord::Base.after_transaction` to ensure the record is visible to the job:
146
+
147
+ ```ruby
148
+ # In a service object
149
+ def call
150
+ ActiveRecord::Base.transaction do
151
+ order = Order.create!(params)
152
+ # Job runs AFTER the transaction commits
153
+ order.run_callbacks(:commit) { OrderConfirmationJob.perform_later(order.id) }
154
+ end
155
+ end
156
+
157
+ # Or in the model
158
+ after_commit :send_confirmation, on: :create
159
+
160
+ def send_confirmation
161
+ OrderConfirmationJob.perform_later(id)
162
+ end
163
+ ```
164
+
165
+ **Unique jobs (prevent duplicates):**
166
+ Use `sidekiq-unique-jobs` or check within the job:
167
+
168
+ ```ruby
169
+ class IndexCodebaseJob < ApplicationJob
170
+ def perform(project_id)
171
+ project = Project.find(project_id)
172
+ return if project.indexing? # Already running
173
+
174
+ project.update!(indexing: true)
175
+ # ... do work ...
176
+ project.update!(indexing: false)
177
+ end
178
+ end
179
+ ```
180
+
181
+ **Long-running jobs:**
182
+ Break into smaller jobs that each process a chunk:
183
+
184
+ ```ruby
185
+ class BulkImportJob < ApplicationJob
186
+ def perform(file_path, offset: 0, batch_size: 1000)
187
+ rows = CSV.read(file_path)[offset, batch_size]
188
+ return if rows.blank?
189
+
190
+ rows.each { |row| import_row(row) }
191
+
192
+ # Enqueue next batch
193
+ BulkImportJob.perform_later(file_path, offset: offset + batch_size, batch_size: batch_size)
194
+ end
195
+ end
196
+ ```
197
+
198
+ **Testing jobs:**
199
+ Test the job logic directly with `perform_now`. Test the enqueuing separately.
200
+
201
+ ```ruby
202
+ it "sends confirmation" do
203
+ order = create(:order)
204
+ expect { OrderConfirmationJob.perform_now(order.id) }
205
+ .to change { ActionMailer::Base.deliveries.count }.by(1)
206
+ end
207
+
208
+ it "enqueues on order creation" do
209
+ expect { create(:order) }
210
+ .to have_enqueued_job(OrderConfirmationJob)
211
+ end
212
+ ```