@pilllesss/yorn 1.0.182 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,964 @@
1
+ # Ruby and Rails Code Review Guide
2
+
3
+ > Code review guidance for Ruby 3.4+/4.0 and Rails 8.x, with emphasis on Ruby semantics, controller boundaries, Active Record correctness, query performance, background jobs, and tests.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Scope and Version Awareness](#scope-and-version-awareness)
8
+ - [Ruby Semantics and API Contracts](#ruby-semantics-and-api-contracts)
9
+ - [Collections, Mutation, and Nil](#collections-mutation-and-nil)
10
+ - [Exceptions and Resource Safety](#exceptions-and-resource-safety)
11
+ - [Rails Controllers and Security](#rails-controllers-and-security)
12
+ - [Active Record Correctness](#active-record-correctness)
13
+ - [Query Performance](#query-performance)
14
+ - [Transactions and Concurrency](#transactions-and-concurrency)
15
+ - [Active Job and External Side Effects](#active-job-and-external-side-effects)
16
+ - [Testing and Tooling](#testing-and-tooling)
17
+ - [Review Checklist](#review-checklist)
18
+ - [References](#references)
19
+
20
+ ---
21
+
22
+ ## Scope and Version Awareness
23
+
24
+ This guide targets maintained Ruby 3.4/4.0 applications and Rails 8.x. Before applying version-specific advice, inspect `.ruby-version`, `Gemfile.lock`, `config.load_defaults`, the database adapter, queue adapter, and CI matrix.
25
+
26
+ Do not require a newer API only because it exists. For example, Rails 8's `params.expect` is a concise strong-parameters API, while an existing explicit `require(...).permit(...)` contract can still be correct. Ruby implementation details also differ across MRI, JRuby, and TruffleRuby, so do not treat MRI's Global VM Lock as a substitute for synchronization.
27
+
28
+ Review questions:
29
+ - Which Ruby, Rails, database, and queue versions are actually deployed?
30
+ - Does the change rely on a default that differs across Rails versions or adapters?
31
+ - Are upgrade-only recommendations separated from correctness or security findings?
32
+
33
+ ---
34
+
35
+ ## Ruby Semantics and API Contracts
36
+
37
+ ### Remember Ruby Truthiness
38
+
39
+ Only `false` and `nil` are falsey. Values such as `0`, `""`, and `[]` are truthy, so code translated from other languages can silently choose the wrong branch.
40
+
41
+ ```ruby
42
+ # Bad: zero is truthy, so this does not test whether any rows were found.
43
+ if relation.count
44
+ publish_report
45
+ end
46
+
47
+ # Good: state the condition directly and let the database answer efficiently.
48
+ publish_report if relation.exists?
49
+ ```
50
+
51
+ Review questions:
52
+ - Does a condition rely on `0`, an empty string, or an empty collection being falsey?
53
+ - Would `empty?`, `any?`, `exists?`, `present?`, or an explicit comparison communicate the intent?
54
+ - Is a database relation being loaded only to test whether a row exists?
55
+
56
+ ### Preserve Predicate and Comparison Contracts
57
+
58
+ Predicate methods should normally end in `?` and return a boolean. Review custom equality carefully: `==`, `eql?`, `equal?`, and `hash` have different contracts.
59
+
60
+ ```ruby
61
+ class Money
62
+ attr_reader :amount, :currency
63
+
64
+ def initialize(amount, currency)
65
+ @amount = amount
66
+ @currency = currency
67
+ end
68
+
69
+ def ==(other)
70
+ other.is_a?(Money) &&
71
+ amount == other.amount &&
72
+ currency == other.currency
73
+ end
74
+
75
+ alias eql? ==
76
+
77
+ def hash
78
+ [amount, currency].hash
79
+ end
80
+ end
81
+ ```
82
+
83
+ Review questions:
84
+ - If `eql?` is overridden, is `hash` consistent so Hash and Set lookups work?
85
+ - Is `equal?` being used accidentally where value equality is intended?
86
+ - Does a predicate return a domain object or `nil` when callers expect `true` or `false`?
87
+
88
+ ### Keep Keyword Arguments Explicit
89
+
90
+ Ruby separates positional and keyword arguments. Broad `**options` parameters can hide typos and weaken public API contracts.
91
+
92
+ ```ruby
93
+ # Bad: silently accepts misspelled or unsupported options.
94
+ def charge(customer, **options)
95
+ gateway.charge(customer, options)
96
+ end
97
+
98
+ # Good: required and optional keywords are visible to callers.
99
+ def charge(customer, amount:, currency: "USD", idempotency_key:)
100
+ gateway.charge(
101
+ customer,
102
+ amount: amount,
103
+ currency: currency,
104
+ idempotency_key: idempotency_key
105
+ )
106
+ end
107
+ ```
108
+
109
+ Use `...` for transparent delegation only when forwarding every positional argument, keyword argument, and block is intentional.
110
+
111
+ Review questions:
112
+ - Are required keywords declared without defaults?
113
+ - Does a wrapper preserve keywords and blocks correctly?
114
+ - Would accepting arbitrary keywords turn a caller typo into delayed or silent behavior?
115
+
116
+ ### Keep Metaprogramming Boundaries Small
117
+
118
+ Ruby makes dynamic APIs easy to build, but reviewers should treat `eval`, `class_eval` with strings, `send`, `const_get`, and dynamic constantization as trust boundaries.
119
+
120
+ ```ruby
121
+ # Bad: user input controls the method that is invoked.
122
+ account.send(params[:operation])
123
+
124
+ # Good: map external input to a fixed internal operation.
125
+ OPERATIONS = {
126
+ "activate" => :activate!,
127
+ "suspend" => :suspend!
128
+ }.freeze
129
+
130
+ operation = OPERATIONS.fetch(params[:operation])
131
+ account.public_send(operation)
132
+ ```
133
+
134
+ Review questions:
135
+ - Can external input select a method, class, constant, template, or code string?
136
+ - Is a fixed allowlist used before `public_send` or `constantize`?
137
+ - Is metaprogramming isolated and covered by contract tests?
138
+
139
+ ### Treat Deserialization and Shell Execution as Trust Boundaries
140
+
141
+ `Marshal.load` can instantiate Ruby objects and must not receive untrusted bytes. Prefer data-only formats such as JSON, and use `YAML.safe_load` or `Psych.safe_load` with an explicit allowlist when YAML is required. Build process arguments as separate values instead of interpolating a shell command.
142
+
143
+ ```ruby
144
+ # Bad: untrusted input can trigger unsafe object deserialization.
145
+ payload = Marshal.load(request.raw_post)
146
+
147
+ # Good: parse data-only JSON values, then validate their shape.
148
+ payload = JSON.parse(request.raw_post)
149
+
150
+ # Good: permit only the YAML types and aliases the format requires.
151
+ payload = YAML.safe_load(request.raw_post, permitted_classes: [], aliases: false)
152
+
153
+ # Bad: a filename can inject additional shell syntax.
154
+ system("convert #{uploaded_path} output.png")
155
+
156
+ # Better: argv form bypasses shell parsing, but not path validation.
157
+ system("convert", uploaded_path, "output.png")
158
+ ```
159
+
160
+ Review questions:
161
+ - Can request, cache, cookie, queue, or file data reach `Marshal.load`, `YAML.load`, or unrestricted `Psych.load`?
162
+ - Is parsed data validated before it selects a class, method, path, or database operation?
163
+ - Are `permitted_classes` and `aliases` as restrictive as the YAML contract allows?
164
+ - Are subprocess arguments passed separately, with the path constrained to the intended upload root and exit status/timeouts handled?
165
+
166
+ ---
167
+
168
+ ## Collections, Mutation, and Nil
169
+
170
+ ### Avoid Shared Collection Defaults
171
+
172
+ `Hash.new(object)` reuses the same object for every missing key. Use a block when each key needs independent mutable state.
173
+
174
+ ```ruby
175
+ # Bad: all missing keys share one array.
176
+ grouped = Hash.new([])
177
+ grouped[:paid] << 1
178
+ grouped[:failed] << 2
179
+ # The keys were never assigned; both reads mutated the same hidden default.
180
+ grouped # => {}
181
+ grouped[:paid] # => [1, 2]
182
+ grouped[:failed] # => [1, 2]
183
+
184
+ # Bad: every element refers to the same array.
185
+ matrix = Array.new(3, [])
186
+
187
+ # Good: the block creates an independent array for each element.
188
+ matrix = Array.new(3) { [] }
189
+
190
+ # Good: each missing key receives its own array.
191
+ grouped = Hash.new { |hash, key| hash[key] = [] }
192
+ grouped[:paid] << 1
193
+ grouped[:failed] << 2
194
+ ```
195
+
196
+ Review questions:
197
+ - Does a Hash default contain a mutable Array, Hash, or String?
198
+ - Is the default block storing the generated value back into the hash when that is intended?
199
+ - Could a shared constant or class attribute be mutated across requests or tests?
200
+
201
+ ### Treat Bang Methods as Semantic Signals, Not Guarantees
202
+
203
+ Ruby's `!` convention usually means a more dangerous or mutating counterpart, but it does not universally mean "raises on failure." Many mutating methods return `nil` when no change was made.
204
+
205
+ ```ruby
206
+ name = "ready"
207
+
208
+ # Bad: `downcase!` returns nil when the string is already lowercase.
209
+ normalized = name.downcase!
210
+
211
+ # Good: use the non-bang form when the return value is the result.
212
+ normalized = name.downcase
213
+ ```
214
+
215
+ Review questions:
216
+ - Is code relying on a bang method's return value without checking its contract?
217
+ - Is in-place mutation visible to every owner of the object?
218
+ - Would a non-mutating transformation make the data flow clearer?
219
+
220
+ ### Do Not Use Safe Navigation to Hide Broken Invariants
221
+
222
+ `&.` is useful for genuinely optional relationships. Repeated safe navigation can also turn missing required data into a late `nil` and hide the source of an invalid state.
223
+
224
+ ```ruby
225
+ # Bad when every order must have a customer and email.
226
+ recipient = order&.customer&.email
227
+
228
+ # Good: enforce required associations and fail close to the invalid state.
229
+ recipient = order.customer.email
230
+ ```
231
+
232
+ Review questions:
233
+ - Is nil part of the domain, or is it evidence of a broken invariant?
234
+ - Should the model, database, or caller guarantee presence instead?
235
+ - Does `dig` or `&.` make an error disappear only for it to fail later?
236
+
237
+ ### Choose the Enumerable Operation That Matches the Intent
238
+
239
+ Use `each` for side effects, `map` for transformation, `filter_map` for transform-and-compact, and `each_with_object` for accumulation. Avoid chains that allocate intermediate arrays in hot paths.
240
+
241
+ ```ruby
242
+ # Bad: uses map for side effects and leaves an unused array behind.
243
+ orders.map { |order| AuditLog.write(order) }
244
+
245
+ # Good: side effect is explicit.
246
+ orders.each { |order| AuditLog.write(order) }
247
+
248
+ # Good: transform and discard nil values in one pass.
249
+ emails = users.filter_map { |user| user.email if user.subscribed? }
250
+ ```
251
+
252
+ Review questions:
253
+ - Is `map` used when its returned collection is ignored?
254
+ - Does a long chain allocate large intermediate collections?
255
+ - Is a relation converted to an Array before the database has applied filtering, ordering, or aggregation?
256
+
257
+ ---
258
+
259
+ ## Exceptions and Resource Safety
260
+
261
+ ### Rescue the Smallest Expected Failure
262
+
263
+ A bare `rescue` catches `StandardError` and its subclasses. It still combines many unrelated failures, including programming errors, database errors, timeouts, and validation errors.
264
+
265
+ ```ruby
266
+ # Bad: converts every ordinary application failure into the same response.
267
+ def create
268
+ order = Orders::Create.call(order_params)
269
+ render json: order
270
+ rescue => error
271
+ Rails.logger.info(error.message)
272
+ render json: { error: error.message }, status: :internal_server_error
273
+ end
274
+
275
+ # Good: translate expected errors at the boundary and let unexpected errors
276
+ # reach centralized reporting.
277
+ rescue_from Orders::InvalidOrder, with: :render_invalid_order
278
+
279
+ def create
280
+ order = Orders::Create.call(order_params)
281
+ render json: { id: order.id, status: order.status }, status: :created
282
+ end
283
+ ```
284
+
285
+ Review questions:
286
+ - Is the rescued exception expected at this boundary?
287
+ - Is the protected block narrow enough to avoid catching unrelated bugs?
288
+ - Does the response expose an internal exception message, SQL, path, token, or vendor detail?
289
+
290
+ ### Preserve the Original Backtrace and Cause
291
+
292
+ Use bare `raise` to re-raise the current exception. `raise error` also re-raises the same exception object and preserves its existing backtrace, but bare `raise` makes that intent clearer. When translating to a domain error, report the original exception object and keep its cause chain.
293
+
294
+ ```ruby
295
+ begin
296
+ gateway.charge(payload)
297
+ rescue Gateway::Timeout => error
298
+ Rails.error.report(error, context: { order_id: order.id })
299
+ raise Payments::Unavailable, "payment gateway timed out"
300
+ end
301
+ ```
302
+
303
+ Inside a `rescue` block, raising a new exception keeps the rescued exception as its implicit `cause`. Constructing a new exception, such as `raise Payments::Unavailable, error.message`, creates a new backtrace; do that only when the boundary needs a domain-specific error.
304
+
305
+ See [Error Handling Guide](cross-cutting/error-handling-principles.md) for boundary, cause-chain, and reporting principles shared across ecosystems.
306
+
307
+ Review questions:
308
+ - Is structured context logged without secrets or full payment data?
309
+ - Does the error retain a useful cause chain?
310
+ - Is the log level appropriate, and will the error be reported exactly once?
311
+
312
+ ### Use Blocks or Ensure for Cleanup
313
+
314
+ Prefer block-based resource APIs because they close resources even when an exception is raised. Use `ensure` when no block form exists.
315
+
316
+ ```ruby
317
+ # Good: File.open closes the handle after the block.
318
+ File.open(path, "rb") do |file|
319
+ checksum(file)
320
+ end
321
+
322
+ connection = pool.checkout
323
+ begin
324
+ consume(connection)
325
+ ensure
326
+ pool.checkin(connection)
327
+ end
328
+ ```
329
+
330
+ Review questions:
331
+ - Are files, locks, temporary directories, and checked-out resources released on every path?
332
+ - Does an `ensure` block accidentally `return` and suppress an exception?
333
+ - Is retry logic bounded and limited to transient failures?
334
+
335
+ ---
336
+
337
+ ## Rails Controllers and Security
338
+
339
+ ### Require and Permit Parameters Explicitly
340
+
341
+ Rails 8 provides `params.expect` to require the expected shape and allow only named attributes. For older supported Rails versions, follow the established `require(...).permit(...)` convention.
342
+
343
+ ```ruby
344
+ # Bad: unfiltered controller parameters at a mass-assignment boundary.
345
+ Order.create!(params[:order])
346
+
347
+ # Bad: permits current and future attributes, including sensitive columns.
348
+ params.expect(order: {})
349
+
350
+ # Good: allowlist the flat request contract.
351
+ def order_params
352
+ params.expect(order: [:product_id, :quantity, :shipping_address_id])
353
+ end
354
+
355
+ # Bad: a single array does not express an array of nested parameter hashes.
356
+ params.expect(order: [:product_id, line_items_attributes: [:id, :sku, :quantity]])
357
+
358
+ # Good: use the double-array form for nested resource arrays.
359
+ params.expect(order: [
360
+ :product_id,
361
+ line_items_attributes: [[:id, :sku, :quantity]]
362
+ ])
363
+ ```
364
+
365
+ Review questions:
366
+ - Are authorization-sensitive fields such as `user_id`, `role`, `paid`, or `admin` excluded?
367
+ - Do nested resource arrays use the `[[...]]` form, rather than treating them as a flat nested hash?
368
+ - Are nested hashes and arrays permitted with the exact expected shape?
369
+ - Is `permit!`, `to_unsafe_h`, or an empty hash permission widening the contract?
370
+
371
+ ### Parameterize Active Record Queries
372
+
373
+ Never interpolate request data into SQL fragments. Prefer hash conditions or placeholders, and allowlist dynamic identifiers such as sort columns.
374
+
375
+ ```ruby
376
+ # Bad: SQL injection.
377
+ Order.where("status = '#{params[:status]}'")
378
+
379
+ # Good: hash conditions are parameterized.
380
+ Order.where(status: params[:status])
381
+
382
+ # Good: placeholders for non-equality predicates.
383
+ Order.where("total_cents >= ?", params[:minimum_cents])
384
+
385
+ # Bad: values are quoted, but the column name is still attacker-controlled.
386
+ Order.order(Arel.sql("#{params[:sort]} DESC"))
387
+
388
+ # Good: map external values to fixed SQL identifiers.
389
+ SORTS = {
390
+ "newest" => { created_at: :desc },
391
+ "total" => { total_cents: :desc }
392
+ }.freeze
393
+
394
+ Order.order(SORTS.fetch(params[:sort], SORTS.fetch("newest")))
395
+ ```
396
+
397
+ Review questions:
398
+ - Does user input reach `where`, `order`, `select`, `joins`, `having`, or `find_by_sql` as a string?
399
+ - Is `Arel.sql` used only for a developer-controlled literal?
400
+ - Are dynamic columns and directions selected from an allowlist?
401
+
402
+ See [SQL Injection Guide](cross-cutting/sql-injection-prevention.md) for parameterization and dynamic-identifier patterns across ORMs.
403
+
404
+ ### Keep Rendering and Redirects on an Allowlist
405
+
406
+ Rendering an Active Record object directly can expose newly added columns without a controller change. Use a serializer or explicit field list. Treat redirects, file paths, and HTML safety overrides as security boundaries.
407
+
408
+ ```ruby
409
+ # Bad: future columns can become part of the API response.
410
+ render json: order
411
+
412
+ # Good: response fields are intentional and versionable.
413
+ render json: {
414
+ id: order.id,
415
+ status: order.status,
416
+ total_cents: order.total_cents
417
+ }, status: :created
418
+
419
+ # Bad: open redirect when a user controls the destination.
420
+ redirect_to params[:return_to], allow_other_host: true
421
+
422
+ # Good: redirect to an application route.
423
+ redirect_to order_path(order)
424
+ ```
425
+
426
+ Review questions:
427
+ - Are secrets, password digests, tokens, internal notes, or personal data serialized?
428
+ - Does `html_safe`, `raw`, or `safe_join` receive untrusted content?
429
+ - Can user input control an external redirect, file download path, or response header?
430
+
431
+ ### Keep Authentication, Authorization, and Scoping Separate
432
+
433
+ Authentication establishes who the caller is; authorization decides what that caller may do. Loading a record by global ID before authorization can create an insecure direct object reference.
434
+
435
+ ```ruby
436
+ # Bad: any authenticated user may be able to load another user's order.
437
+ order = Order.find(params[:id])
438
+
439
+ # Good: scope the lookup through the authorized owner or policy scope.
440
+ order = current_user.orders.find(params[:id])
441
+ ```
442
+
443
+ Review questions:
444
+ - Is every member action authorized, including newly added controller actions?
445
+ - Is the record lookup scoped before update, destroy, download, or enqueue?
446
+ - For browser sessions, are state-changing requests protected against CSRF?
447
+
448
+ ### Protect Session-Backed Browser Requests
449
+
450
+ CSRF protection is required when a browser automatically sends an authenticated session cookie. API-only endpoints that use an explicit bearer token can use a different strategy, but disabling CSRF protection is not safe merely because an endpoint returns JSON.
451
+
452
+ ```ruby
453
+ # Good: session-backed controllers keep forgery protection enabled.
454
+ class ApplicationController < ActionController::Base
455
+ protect_from_forgery with: :exception
456
+ end
457
+ ```
458
+
459
+ ```ruby
460
+ # Good: configure the session store in an initializer (for example
461
+ # config/initializers/session_store.rb), not inside a controller class.
462
+ # Keep cookies unavailable to JavaScript, HTTPS-only in production, and make
463
+ # the cross-site policy explicit for the application's flows.
464
+ Rails.application.config.session_store :cookie_store,
465
+ key: "_app_session",
466
+ secure: Rails.env.production?,
467
+ httponly: true,
468
+ same_site: :lax
469
+ ```
470
+
471
+ Review questions:
472
+ - Does any browser-authenticated `POST`, `PATCH`, `PUT`, or `DELETE` skip `protect_from_forgery`?
473
+ - Are `secure`, `httponly`, and `same_site` cookie settings appropriate for the deployment and login flows?
474
+ - Does an API-only endpoint avoid cookie authentication, or otherwise use a deliberate CSRF defense?
475
+
476
+ ### Review Active Storage and Server-Side Fetches
477
+
478
+ For Active Storage uploads and any server-side URL fetch, verify blob ownership, content-type and size validation, and SSRF controls before accepting a user-controlled URL. Signed/expiring URLs, direct-upload limits, and variant parameters should stay under an allowlist. See the [Security Review Guide](security-review-guide.md) for broader request and asset review guidance.
479
+
480
+ Review questions:
481
+ - Can an attachment download, redirect, or server-side fetch access a file or URL outside the authorized scope?
482
+ - Are content type, size, and ownership checked before persisting or transforming a blob?
483
+ - Does a user-controlled URL used for server-side fetching enforce host allowlists and block private network ranges?
484
+
485
+ ### Make Retried Write Requests Idempotent
486
+
487
+ A client or proxy can retry a request after the database commits but before it receives the response. For operations such as order creation, require a caller-supplied idempotency key, scope it to the authenticated principal and operation, and enforce uniqueness in the database.
488
+
489
+ ```ruby
490
+ # The service stores both the key and a fingerprint of the validated request.
491
+ order = Orders::CreateOnce.call(
492
+ actor: current_user,
493
+ idempotency_key: request.headers.fetch("Idempotency-Key"),
494
+ attributes: order_params
495
+ )
496
+
497
+ # Migration: require the key when this API requires the header, then close
498
+ # concurrent duplicate-create races. On an existing populated table, backfill
499
+ # or supply a temporary default before adding `null: false`.
500
+ add_column :orders, :idempotency_key, :string, null: false
501
+ add_index :orders, [:user_id, :idempotency_key], unique: true
502
+ ```
503
+
504
+ When the same key is reused, return the original result only if the request fingerprint matches. Reject key reuse with different attributes instead of silently returning or mutating the wrong resource.
505
+
506
+ Review questions:
507
+ - Can a timeout or enqueue failure happen after the write commits?
508
+ - Will a caller retry create, payment, invitation, or other non-idempotent work?
509
+ - Is the required key rejected before insert and stored in a `null: false` column? A unique index permits multiple `NULL` values on many adapters.
510
+ - For existing tables, does the migration backfill values before enforcing `null: false`?
511
+ - Is idempotency enforced by a unique constraint and tested under concurrent requests?
512
+
513
+ ---
514
+
515
+ ## Active Record Correctness
516
+
517
+ ### Pair Model Validation With Database Constraints
518
+
519
+ Model validations improve error messages but do not protect against concurrent writers or non-Rails clients. Important invariants need database constraints and indexes.
520
+
521
+ ```ruby
522
+ class Membership < ApplicationRecord
523
+ validates :user_id, uniqueness: { scope: :team_id }
524
+ end
525
+
526
+ # Migration also required:
527
+ add_index :memberships, [:team_id, :user_id], unique: true
528
+ add_check_constraint :orders, "total_cents >= 0", name: "orders_total_nonnegative"
529
+ ```
530
+
531
+ Review questions:
532
+ - Do uniqueness, non-null, foreign-key, and range invariants exist in the database?
533
+ - Does application code handle the constraint violation from a concurrent request?
534
+ - Is a new query pattern backed by an appropriate index?
535
+
536
+ ### Know Which APIs Skip Validations and Callbacks
537
+
538
+ Bulk methods are valuable, but methods such as `update_all`, `delete_all`, `insert_all`, and direct SQL bypass parts of the ordinary model lifecycle.
539
+
540
+ ```ruby
541
+ # This does not run model validations or update callbacks.
542
+ Order.where(expired: true).update_all(status: "cancelled", updated_at: Time.current)
543
+ ```
544
+
545
+ Review questions:
546
+ - Is skipping validations, callbacks, timestamps, auditing, and dependent behavior intentional?
547
+ - Would `destroy_all` be required for dependent cleanup, despite being slower?
548
+ - Could a bulk write bypass a counter-cache callback and require `reset_counters` or another explicit reconciliation step?
549
+ - Are bulk operations bounded, observable, and safe to retry?
550
+
551
+ ### Keep Callbacks Small and Predictable
552
+
553
+ Callbacks that send emails, call APIs, enqueue multiple workflows, or mutate unrelated models make persistence hard to reason about.
554
+
555
+ Prefer explicit application services for orchestration. If work must happen only after a transaction commits, use `after_commit` or enqueue-after-commit behavior deliberately.
556
+
557
+ Review questions:
558
+ - Can saving a model unexpectedly trigger network I/O or a large cascade?
559
+ - Could a callback run during tests, data migrations, console scripts, or retries?
560
+ - Is callback ordering part of an undocumented correctness dependency?
561
+
562
+ ### Treat Enum and Scope Changes as Data Contract Changes
563
+
564
+ Integer-backed enums depend on stable ordinal mappings. Append new values or use explicit mappings; do not reorder existing entries.
565
+
566
+ ```ruby
567
+ # Safer for long-lived data and cross-service contracts.
568
+ enum :status, {
569
+ pending: 0,
570
+ paid: 1,
571
+ cancelled: 2
572
+ }
573
+ ```
574
+
575
+ Scopes should return relations consistently. A conditional scope that returns `nil` or an Array breaks composability.
576
+
577
+ Review questions:
578
+ - Does an enum change reinterpret existing rows?
579
+ - Does a scope remain chainable for every input?
580
+ - Is a `default_scope` hiding records or ordering in surprising contexts?
581
+
582
+ ### Keep Counter Caches and Query Caches Correct
583
+
584
+ Counter caches are denormalized data maintained by callbacks. Bulk writes, direct SQL, imports, and deleted rows that bypass the normal lifecycle can make them drift; repair deliberately with `reset_counters` after verifying the source-of-truth query.
585
+
586
+ Long-running jobs should also avoid assuming a query result remains current for the whole job. Check the query-cache scope and use an uncached block or a fresh query when later steps must observe writes or concurrent changes.
587
+
588
+ Review questions:
589
+ - Can `update_all`, `delete_all`, imports, or direct SQL bypass a counter-cache update?
590
+ - Is a counter-cache repair observable and based on the current source of truth?
591
+ - Could a cached query result become stale across phases of a long-running job?
592
+
593
+ ---
594
+
595
+ ## Query Performance
596
+
597
+ ### Detect and Prevent N+1 Queries
598
+
599
+ Association access inside a loop is a review hotspot. Choose eager-loading behavior based on whether the association is only loaded or also used in SQL conditions.
600
+
601
+ ```ruby
602
+ # Bad: one query for orders, then one query per customer.
603
+ orders = Order.where(status: "paid")
604
+ orders.each { |order| puts order.customer.name }
605
+
606
+ # Good: usually two queries, with predictable object loading.
607
+ orders = Order.where(status: "paid").preload(:customer)
608
+
609
+ # Good when Rails should select an eager-loading strategy for access.
610
+ orders = Order.includes(:customer).where(status: "paid")
611
+
612
+ # Use eager_load when a LEFT OUTER JOIN is intentionally required.
613
+ orders = Order.eager_load(:customer).where(customers: { active: true })
614
+ ```
615
+
616
+ `strict_loading` can turn accidental lazy loads into visible failures in development or tests.
617
+
618
+ `eager_load` uses a `LEFT OUTER JOIN`. It can change result cardinality when it joins a collection association: a parent may appear once per matching child. Use `distinct`, a subquery, or separate the filtering query from `preload` when the caller needs one parent row per record.
619
+
620
+ See [N+1 Queries Guide](cross-cutting/n-plus-one-queries.md) for cross-framework detection and loading strategies.
621
+
622
+ Review questions:
623
+ - Does a serializer, view, GraphQL resolver, or job walk unloaded associations?
624
+ - Is the chosen eager-loading method compatible with filtering, ordering, and result cardinality?
625
+ - Could a collection join duplicate parent rows or require `distinct`, a subquery, or a separate `preload` step?
626
+ - Are query-count assertions or strict loading protecting important endpoints?
627
+
628
+ ### Keep Filtering and Aggregation in the Database
629
+
630
+ Loading records before filtering wastes memory and can change semantics.
631
+
632
+ ```ruby
633
+ # Bad: loads every paid order and filters in Ruby.
634
+ large_orders = Order.paid.to_a.select { |order| order.total_cents >= 10_000 }
635
+
636
+ # Good: database applies the predicate.
637
+ large_orders = Order.paid.where(total_cents: 10_000..)
638
+
639
+ # Bad: instantiates records just to read one column.
640
+ emails = User.active.map(&:email)
641
+
642
+ # Good: reads only the requested column.
643
+ emails = User.active.pluck(:email)
644
+ ```
645
+
646
+ Review questions:
647
+ - Is `to_a`, `map`, `select`, or `sort_by` forcing work into Ruby too early?
648
+ - Would `pluck`, `pick`, `ids`, `exists?`, `count`, `sum`, or `maximum` avoid model instantiation?
649
+ - Does the selected column, SQL expression, adapter, or custom attribute type preserve the value type the caller expects?
650
+ - Does the query select more columns or rows than the caller needs?
651
+
652
+ ### Batch Large Data Sets and Paginate Endpoints
653
+
654
+ Use `find_each` or `in_batches` for large background processing, and use stable pagination for list endpoints.
655
+
656
+ ```ruby
657
+ Order.where(status: "pending").find_each(batch_size: 1_000) do |order|
658
+ Reconciliation.check(order)
659
+ end
660
+ ```
661
+
662
+ `find_each` and `in_batches` batch by a cursor (the primary key by default) and can ignore or replace a relation's custom `ORDER BY`. Use an explicit cursor/order supported by the current Rails version, or a dedicated query, when business ordering matters.
663
+
664
+ Review questions:
665
+ - Can this query grow without a bound?
666
+ - Is batch processing compatible with its cursor, ordering, and mutation behavior?
667
+ - Does pagination use a deterministic order and an indexed cursor or key?
668
+
669
+ ### Review Caching With Invalidation in Mind
670
+
671
+ Caching can hide N+1 queries while introducing stale or cross-tenant data. Cache keys must include every input that changes the result.
672
+
673
+ Review questions:
674
+ - Does the key include tenant, locale, authorization scope, and versioned data?
675
+ - Is invalidation tied to the records that affect the cached value?
676
+ - Could sensitive data be served to another user or tenant?
677
+
678
+ ---
679
+
680
+ ## Transactions and Concurrency
681
+
682
+ ### Keep Transactions Focused on Database State
683
+
684
+ Database transactions do not roll back HTTP calls, messages already delivered to an external broker, files, or payments.
685
+
686
+ ```ruby
687
+ # Bad: payment can succeed even if the database transaction rolls back.
688
+ Order.transaction do
689
+ gateway.charge(order.total_cents)
690
+ order.update!(status: "paid")
691
+ end
692
+
693
+ # Better: configure this job to enqueue only after commit, then persist an
694
+ # explicit transition and reconcile the idempotent external operation.
695
+ class PaymentJob < ApplicationJob
696
+ self.enqueue_after_transaction_commit = true
697
+ end
698
+
699
+ Order.transaction do
700
+ order.update!(status: "payment_pending")
701
+ PaymentJob.perform_later(order)
702
+ end
703
+ ```
704
+
705
+ Do not rely on a version-dependent enqueue default. Rails 8.0/8.1 applications and queue adapters can enqueue immediately depending on `config.load_defaults` and deployment topology; newer Rails defaults may defer more often, but the job should declare the behavior it requires. If the enqueue itself must be durable with the state change, use an outbox or a reconciliation path.
706
+
707
+ Review questions:
708
+ - Which side effects are actually covered by the transaction?
709
+ - Does the exact job class declare post-commit enqueue behavior, rather than relying on an adapter or Rails-version default?
710
+ - Could a timeout mean "failed" or "succeeded but the response was lost"?
711
+ - Is there a recovery or reconciliation path for partial completion?
712
+
713
+ ### Do Not Swallow Database Errors Inside a Broken Transaction
714
+
715
+ Some adapters, notably PostgreSQL, leave a transaction unusable after a statement error until it is rolled back. Catch expected constraint errors outside the transaction boundary or restart the whole transaction deliberately.
716
+
717
+ Review questions:
718
+ - Is `ActiveRecord::StatementInvalid` rescued inside a transaction and then followed by more SQL?
719
+ - Is retry limited to known transient conflicts such as deadlocks or serialization failures?
720
+ - Does retry repeat an external side effect?
721
+
722
+ ### Lock the Invariant, Not Just the Code Path
723
+
724
+ Ruby process locks do not protect data across multiple processes or hosts. Use unique constraints, atomic updates, optimistic locking, or row locks for shared database invariants.
725
+
726
+ ```ruby
727
+ order.with_lock do
728
+ return if order.paid?
729
+
730
+ order.update!(status: "payment_pending")
731
+ end
732
+ ```
733
+
734
+ Review questions:
735
+ - Can two requests observe the same old state and both proceed?
736
+ - Would an atomic conditional update or unique index be simpler than a lock?
737
+ - Is lock ordering consistent to avoid deadlocks?
738
+
739
+ ### Choose Optimistic or Pessimistic Locking Deliberately
740
+
741
+ Use optimistic locking for ordinary edits where conflicts are uncommon and the caller can reload or resolve a conflict. Use a short pessimistic lock such as `with_lock` for a small critical state transition that needs serialized access.
742
+
743
+ ```ruby
744
+ # Migration: Rails increments this column and rejects stale updates.
745
+ add_column :orders, :lock_version, :integer, default: 0, null: false
746
+
747
+ begin
748
+ order.update!(shipping_address: new_address)
749
+ rescue ActiveRecord::StaleObjectError
750
+ # Reload, return a conflict response, or ask the user to reconcile changes.
751
+ end
752
+ ```
753
+
754
+ Review questions:
755
+ - Can a low-contention user edit use `lock_version` and a conflict response instead of holding a row lock?
756
+ - Does pessimistic locking cover only the minimal state transition and preserve a consistent lock order?
757
+
758
+ ### Treat Mutable Global State as Concurrent State
759
+
760
+ Class variables, class instance variables, constants containing mutable objects, memoization, and singleton clients may be shared by request threads.
761
+
762
+ Review questions:
763
+ - Is lazy initialization thread-safe and safe during code reload?
764
+ - Is request-specific data stored in a global, class variable, or long-lived thread local?
765
+ - Are shared clients documented as thread-safe by their library?
766
+
767
+ ---
768
+
769
+ ## Active Job and External Side Effects
770
+
771
+ ### Make Retried Jobs Idempotent
772
+
773
+ Jobs may be retried when configured by Active Job or the queue backend. A process can also stop after an external side effect succeeds but before local state is updated.
774
+
775
+ ```ruby
776
+ class CapturePaymentJob < ApplicationJob
777
+ self.enqueue_after_transaction_commit = true
778
+ retry_on PaymentGateway::Timeout, wait: :polynomially_longer, attempts: 5
779
+
780
+ def perform(order)
781
+ order.with_lock do
782
+ return if order.paid?
783
+ order.update!(status: "payment_processing")
784
+ end
785
+
786
+ result = PaymentGateway.capture(
787
+ amount: order.total_cents,
788
+ idempotency_key: "order-#{order.id}-capture"
789
+ )
790
+
791
+ order.update!(status: "paid", payment_reference: result.reference)
792
+ end
793
+ end
794
+ ```
795
+
796
+ The gateway idempotency key, persisted state, and reconciliation process must work together. A database flag alone cannot prove that an external charge did not already happen.
797
+
798
+ Review questions:
799
+ - What happens if the worker stops after the side effect but before the final update?
800
+ - Is the idempotency key stable across retries but unique to the intended operation?
801
+ - Are retryable and permanent failures handled differently?
802
+ - Which states are terminal, retryable, or recoverable, and how does a stuck `payment_processing` state become visible for reconciliation?
803
+
804
+ ### Understand GlobalID Arguments
805
+
806
+ Active Job can serialize Active Record objects with GlobalID. The job loads the record at execution time, not enqueue time. If the record has been deleted, deserialization raises `ActiveJob::DeserializationError` before `perform` runs.
807
+
808
+ When absence is an expected business case, pass an ID and handle `ActiveRecord::RecordNotFound` narrowly inside the job. Do not use a blanket `discard_on ActiveJob::DeserializationError` unless every deserialization failure is intentionally disposable; it can also hide serializer or deployment problems.
809
+
810
+ Review questions:
811
+ - Is the job intentionally using current record state rather than a snapshot?
812
+ - What should happen when the record is deleted or no longer eligible?
813
+ - Does `discard_on ActiveJob::DeserializationError` lose work that should be investigated instead?
814
+
815
+ ### Enqueue With Transaction Boundaries Deliberately
816
+
817
+ Jobs that can run before their records commit may fail to find those records. Rails supports enqueue-after-commit behavior, but transactional guarantees depend on the exact job setting, queue adapter, Rails load defaults, and database topology.
818
+
819
+ Review questions:
820
+ - Can the worker run before the creating transaction commits?
821
+ - Does the code accidentally rely on the job table sharing the application database?
822
+ - If enqueue fails after data commits, is there an outbox, retry, or reconciliation path?
823
+
824
+ ### Put Timeouts and Observability Around Network Work
825
+
826
+ Every external call needs bounded connect/read/write timeouts, structured error reporting, and enough identifiers to trace a retry without logging secrets.
827
+
828
+ Review questions:
829
+ - Are timeouts explicit in the client configuration?
830
+ - Are queue latency, attempts, external request IDs, and final outcomes observable?
831
+ - Is a long-running job split into resumable or checkpointed units when appropriate?
832
+
833
+ ---
834
+
835
+ ## Testing and Tooling
836
+
837
+ ### Test Behavior at the Right Boundary
838
+
839
+ Use model tests for domain rules, request/system tests for controller behavior, and job tests for serialization, retry, and side-effect boundaries. Avoid tests that only assert a callback or private method was invoked.
840
+
841
+ High-value cases include:
842
+ - unpermitted parameters cannot update protected attributes;
843
+ - unsafe sort/filter input cannot alter SQL structure;
844
+ - authorization scopes records before lookup;
845
+ - serializers do not expose sensitive columns;
846
+ - N+1 regressions fail through query-count assertions or strict loading;
847
+ - jobs are safe when performed twice;
848
+ - deleted GlobalID records and permanent gateway failures are handled intentionally;
849
+ - a timeout after a successful external side effect is reconciled without duplication.
850
+
851
+ ### Keep Time and Asynchronous Tests Deterministic
852
+
853
+ Use Rails time helpers instead of real sleeps. Run jobs through the test adapter or a backend-specific integration test, and assert both enqueue behavior and performed behavior where each matters.
854
+
855
+ ```ruby
856
+ travel_to(Time.zone.local(2026, 7, 14, 10, 0, 0)) do
857
+ assert_enqueued_with(job: ExpireOrderJob, at: 30.minutes.from_now) do
858
+ order.schedule_expiration!
859
+ end
860
+ end
861
+ ```
862
+
863
+ Review questions:
864
+ - Does a test depend on wall-clock timing, global order, or previously created data?
865
+ - Are external services replaced at a clear adapter boundary?
866
+ - Do parallel tests share mutable constants, files, ports, or non-transactional state?
867
+
868
+ ### Run the Checks the Project Actually Configures
869
+
870
+ Common commands include:
871
+
872
+ ```bash
873
+ bundle exec ruby -wc path/to/file.rb
874
+ bundle exec rubocop
875
+ bundle exec brakeman -q
876
+ bundle exec rails test
877
+ bundle exec rspec
878
+ ```
879
+
880
+ Run only tools present in the repository, and inspect their configuration before treating style or complexity thresholds as universal rules. Security findings from Brakeman and dependency scanners require human validation, not blind suppression.
881
+
882
+ ---
883
+
884
+ ## Review Checklist
885
+
886
+ ### Ruby Semantics
887
+ - [ ] Conditions account for Ruby truthiness (`0`, `""`, and `[]` are truthy).
888
+ - [ ] Equality and `hash` contracts are consistent.
889
+ - [ ] Keyword arguments and forwarding preserve the intended API contract.
890
+ - [ ] Dynamic method or constant lookup is allowlisted.
891
+ - [ ] Mutable Hash/Array defaults, constants, and class state are not shared accidentally.
892
+ - [ ] Untrusted data never reaches unsafe deserialization or interpolated shell commands.
893
+ - [ ] Bang-method and safe-navigation return semantics are understood.
894
+
895
+ ### Exceptions and Resources
896
+ - [ ] Rescue clauses handle specific expected failures at narrow boundaries.
897
+ - [ ] Unexpected errors retain their cause and backtrace.
898
+ - [ ] Client responses do not expose internal exception messages.
899
+ - [ ] Logs include safe structured context and the exception object.
900
+ - [ ] Resources and locks are released on every path.
901
+ - [ ] Retries are bounded and cannot duplicate non-idempotent work.
902
+
903
+ ### Controllers and Security
904
+ - [ ] Strong parameters use an exact allowlist; no `permit!` or unsafe hash conversion.
905
+ - [ ] Nested resource arrays use `params.expect(...: [[...]])` with the intended shape.
906
+ - [ ] SQL values are parameterized and dynamic identifiers are allowlisted.
907
+ - [ ] Authentication, authorization, and record scoping are all present.
908
+ - [ ] An unscoped `Model.find(params[:id])` cannot bypass ownership or policy checks.
909
+ - [ ] Retried write requests use scoped idempotency keys and database uniqueness where required.
910
+ - [ ] Serialized fields are explicit and exclude sensitive data.
911
+ - [ ] Redirects, HTML safety overrides, files, and headers do not trust user input or permit open redirects.
912
+ - [ ] State-changing browser requests have CSRF protection, and session cookies use intentional `secure`, `httponly`, and `same_site` settings.
913
+ - [ ] Active Storage uploads and server-side URL fetches validate ownership, content, and SSRF boundaries.
914
+
915
+ ### Active Record
916
+ - [ ] Critical model validations are backed by database constraints and indexes.
917
+ - [ ] Bulk APIs intentionally account for skipped callbacks, validations, and timestamps.
918
+ - [ ] Bulk writes cannot silently drift counter caches; repair and reconciliation are explicit.
919
+ - [ ] Callbacks are small and do not hide orchestration or network side effects.
920
+ - [ ] Enum mappings preserve existing persisted values.
921
+ - [ ] Scopes remain relations and compose for every input.
922
+ - [ ] Concurrency invariants use constraints, atomic updates, optimistic locking, or short database locks.
923
+
924
+ ### Queries and Performance
925
+ - [ ] Association access in loops is preloaded or explicitly justified.
926
+ - [ ] `includes`, `preload`, or `eager_load` matches the query semantics.
927
+ - [ ] Collection joins cannot duplicate parent rows or change cardinality unnoticed.
928
+ - [ ] Filtering, sorting, aggregation, and existence checks stay in the database.
929
+ - [ ] Large data sets use batches; list endpoints are paginated with stable ordering.
930
+ - [ ] New filter/join/order patterns have supporting indexes.
931
+ - [ ] Cache keys include tenant, authorization, locale, and data versions as needed.
932
+
933
+ ### Jobs and External Services
934
+ - [ ] Jobs are safe under retry, duplicate delivery, and worker interruption.
935
+ - [ ] External operations use stable idempotency keys where supported.
936
+ - [ ] GlobalID deletion and stale/current-state semantics are intentional; expected absence is handled narrowly.
937
+ - [ ] Enqueue timing is declared on the job and does not rely accidentally on queue/database topology or version defaults.
938
+ - [ ] Partial completion has reconciliation, compensation, or an outbox path.
939
+ - [ ] Terminal, retryable, and stuck processing states are observable and recoverable.
940
+ - [ ] Network calls have timeouts, observability, and secret-safe logging.
941
+
942
+ ### Tests and Automation
943
+ - [ ] Request tests cover parameter filtering, authorization, status codes, and response fields.
944
+ - [ ] Query-count or strict-loading tests protect performance-sensitive paths.
945
+ - [ ] Job tests cover retries, duplicate execution, deletion, and partial failure.
946
+ - [ ] Time-dependent tests use deterministic Rails time helpers.
947
+ - [ ] The repository's configured Ruby, Rails, lint, test, and security checks pass.
948
+
949
+ ---
950
+
951
+ ## References
952
+
953
+ - [Ruby Releases](https://www.ruby-lang.org/en/downloads/releases/)
954
+ - [Ruby Syntax: Methods and Arguments](https://ruby-doc.org/3.4/syntax/methods_rdoc.html)
955
+ - [Ruby Syntax: Exceptions](https://ruby-doc.org/3.4/syntax/exceptions_rdoc.html)
956
+ - [Rails Action Controller Overview](https://guides.rubyonrails.org/action_controller_overview.html)
957
+ - [Rails Active Record Query Interface](https://guides.rubyonrails.org/active_record_querying.html)
958
+ - [Rails Active Record Transactions](https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html)
959
+ - [Rails Active Job Basics](https://guides.rubyonrails.org/active_job_basics.html)
960
+ - [Securing Rails Applications](https://guides.rubyonrails.org/security.html)
961
+ - [Testing Rails Applications](https://guides.rubyonrails.org/testing.html)
962
+ - [RuboCop Documentation](https://docs.rubocop.org/rubocop/)
963
+ - [RuboCop Rails Documentation](https://docs.rubocop.org/rubocop-rails/)
964
+ - [Brakeman](https://brakemanscanner.org/)