git:20260510.fe97ca3 to git:20260905.88628fb

61 added, 62 removed. Audit A to A.

---
name: programming-ruby
title: "Ruby Development"
description: "Idiomatic Ruby and Rails architecture: objects, services, ActiveRecord boundaries, and modern Hotwire patterns. Auto-activates in Ruby projects."
license: Apache-2.0
- compatibility: "Requires Ruby 3.x and Bundler."
+ compatibility: "Requires a supported Ruby and Bundler; Rails guidance applies only to Rails projects."
domains: developer
rules:
- file(Gemfile)
- content(ruby)
---
- ## Mental model
+ ## Overview
- Ruby rewards expressive, intention-revealing code; Rails rewards convention. The maintainable Rails app keeps controllers thin, models focused on persistence and small associated behaviors, and business logic in service objects, form objects, or domain modules. Most pain comes from fat models (every concern bolted on), fat controllers (logic inlined), and callbacks that fire side effects whenever a record saves.
+ Write idiomatic Ruby with explicit data contracts and predictable persistence. Research baseline: 2026-09-05, Ruby 4.0 stable; Rails guidance below targets supported Rails 8 APIs. Check `.ruby-version`, the Gemfile/lockfile, Ruby implementation, and Rails version before using newer features. Preserve existing application conventions and dependencies; a small change does not need a new service layer or gem extraction.
- ## Idiomatic Ruby
+ ## Mental model
- - Expressive method names — `user.active?` over `user.is_active`, `users.empty?` over `users.size == 0`
- - Blocks and iterators express intent — `each`, `map`, `select`, `reduce`, `tap`, `then`
- - Pattern matching (`case/in`) for destructuring nested data; cleaner than chained `if`/`elsif`
- - Safe navigation `&.` for nil-tolerant chains; `dig` for nested hashes/arrays
- - Keyword arguments for clarity on methods with more than one or two parameters
- - Frozen string literals (`# frozen_string_literal: true`) at the top of every file — cheap immutability
- - Modules for shared behavior (mixins) and namespacing; classes for things with state
+ Ruby's flexibility makes names and failure contracts important. Keep domain behavior near the object that owns it; extract orchestration only when it has independent responsibilities. Rails conventions reduce boilerplate, but callbacks, transactions, and asynchronous jobs have distinct timing and failure semantics.
- ## Object design
+ ## Current language features
- - Small, focused classes — a class that does one thing is easier to test and change than a "manager" doing five
- - Plain Ruby objects (POROs) for domain concepts that aren't tables; they don't need to inherit from anything
- - Value objects (`Data.define` in Ruby 3.2+ or `Struct`) for immutable bundles of attributes
- - Duck typing — accept anything that responds to the method you call; don't pre-declare interfaces unless they earn their keep
- - Composition over inheritance; reach for inheritance only when the "is-a" relationship is genuine and stable
+ - Ruby 3.2+ `Data.define` describes fixed value records; members cannot be reassigned, but referenced strings/arrays/hashes are not deeply frozen. `Struct` is mutable unless frozen. Copy/freeze nested values only when the contract needs it.
+ - Ruby 3.4's implicit block parameter `it` suits a short, obvious block; use named parameters for nested or nontrivial logic. New syntax should improve readability rather than demonstrate novelty.
+ - Ruby 4 makes `Set` a core class and adds `Array#rfind` for reverse search. Keep older-version `require "set"` compatibility when the repository supports older Rubies.
+ - Ruby 4's Ruby Box and Ractor remain experimental; ZJIT is also experimental. Do not introduce them or switch JITs as routine cleanup.
+ - Keep frozen-string policy explicit and consistent with the project. `# frozen_string_literal: true` freezes literals, not all strings; use `+""` or an owned duplicate when mutation is intended.
- ## Errors
+ ## Values and control flow
- - Rescue `StandardError`, never `Exception` (it catches `SignalException`, `SystemExit`)
- - Custom exception classes inherit from `StandardError`; name them after the failure (`UserNotFound`, `PaymentDeclined`)
- - Catch specific errors first, then fall back if needed; empty `rescue` blocks are bugs
- - `raise` with a class plus message — `raise UserNotFound, "id=#{id}"`
- - Don't use exceptions for predictable control flow — return `nil`, a Result-shaped value, or use a method ending in `?` / `!` to signal intent
+ - Use keyword arguments where positional arguments hide intent. Forward positional arguments, keywords, and blocks accurately; Ruby 3 distinguishes a positional hash from keyword arguments.
+ - Prefer `fetch` for required hash/configuration keys; use `[]`, `dig`, and `&.` only where absence is valid. A chain of nil-tolerant operations must not conceal a missing invariant.
+ - Ruby treats only `nil` and `false` as falsey. `||=` does not cache a valid false/nil result; use an explicit initialization check when those results matter.
+ - Use explicit parsing such as `Integer(text, 10)` for validated integer input; `to_i` can silently accept malformed data. Keep absence distinct from invalid syntax.
+ - Use `map` for transformation, `each` for effects, and `filter_map` only when discarding both nil and false is intended. Keep long chains readable with named intermediate values.
+ - A `return` inside a normal block exits its enclosing method; an escaped proc can raise `LocalJumpError` when that method has returned. A lambda's `return` exits the lambda. Use `next` for the current block invocation.
+ - Predicate methods conventionally end in `?`; `!` identifies a more dangerous counterpart, not a universal promise to mutate or raise. Follow the specific method contract.
- ## Rails architecture
+ ## Errors and resources
- - Controllers: parse input, call one application method, render — five lines, not fifty
- - Strong Parameters for input filtering; never `params.permit!`
- - Models: persistence, validations, scopes, and behaviors that genuinely belong to the entity; everything else moves out
- - Service objects (`app/services/`) for orchestration: one class per use case (`CreateInvoice`, `ChargePayment`)
- - Form objects when a form spans multiple models or carries validation distinct from persistence
- - Query objects when ActiveRecord scopes start composing into something hard to read
- - Concerns for genuinely shared behavior across multiple models — not as a hiding place for fat-model code
+ - Rescue the specific error where recovery or translation is meaningful. Ordinary custom errors derive from `StandardError`; do not broadly rescue `Exception`, which includes shutdown and interrupt conditions.
+ - Preserve causes when wrapping failures. Avoid rescue modifiers, empty rescues, and fallback empty collections that convert broken I/O or invalid data into success.
+ - Use block forms of resource APIs, or `ensure` when cleanup must span custom control flow. Do not return from `ensure`, which can replace results or suppress errors.
+ - Bound retries to identified transient failures and account for side effects already performed. Prefer an explicit not-found result only where the caller expects one.
- ## ActiveRecord discipline
+ ## Rails persistence and jobs
- - Avoid lifecycle callbacks for side effects (sending email, hitting other services) — they make tests slow and behavior surprising; move them into service objects
- - Validations belong on the model; cross-record consistency belongs in transactions
- - Eager-load relationships explicitly (`includes`, `preload`) to kill N+1
- - Wrap multi-step writes in `transaction do ... end`; handle `ActiveRecord::Rollback` deliberately
- - Migrations are forward-only in production; new changes are new migrations
+ - Keep entity behavior on the model when cohesive; extract a query, form, or orchestration object only when it clarifies a real boundary. Controllers should adapt HTTP and authorize operations, not dictate arbitrary class counts.
+ - Rails 8's `params.expect` can require and permit an expected parameter shape together. Strong Parameters filter assignment; they do not authorize access or validate all domain constraints.
+ - Back uniqueness and cross-record invariants with database constraints and appropriate locking. A model validation alone cannot prevent races. Choose eager loading based on accessed relationships.
+ - Transactions cover database work on their connection; they do not roll back HTTP calls or restore in-memory objects. Use raising persistence methods when failure must abort; ordinary `save` returning false does not trigger rollback automatically.
+ - `ActiveRecord::Rollback` rolls back but is swallowed by the transaction boundary. Do not rescue database statement errors inside a transaction and continue using a potentially aborted transaction.
+ - Schedule external effects after commit when appropriate; use durable coordination when delivery must survive a process crash. Make jobs safe under duplicate execution and bounded retries.
+ - Active Job supports records via GlobalID, which reloads them later. Choose IDs, records, or explicit snapshots according to desired semantics and handle deletion before execution. Verify enqueue-after-commit behavior for the actual adapter/database setup.
- ## Async and background work
+ ## Example
- - ActiveJob with Solid Queue (Rails 8 default), Sidekiq, or Resque — pick one and stick with it
- - Jobs are idempotent — they may run more than once
- - Pass IDs to jobs, not records — the record may have changed by the time the job runs
- - Long-running tasks belong in jobs, not in request handlers
+ Preserve a legitimate false value and reject a malformed required value:
- ## Hotwire and views
+ ```ruby
+ # frozen_string_literal: true
- - Turbo Drive, Frames, and Streams for progressive enhancement — most Rails UI doesn't need a SPA
- - Stimulus controllers for sprinkles of interactivity scoped to elements
- - Partials with locals for reusable templates; ViewComponent or Phlex for component-style views with their own tests
- - I18n for user-facing strings from day one
+ def read_settings(input)
+ enabled = input.fetch("enabled")
+ unless enabled == true || enabled == false
+ raise ArgumentError, "enabled must be boolean"
+ end
- ## Testing
+ attempts = Integer(input.fetch("attempts"), 10)
+ raise ArgumentError, "attempts must be positive" unless attempts.positive?
- - RSpec or Minitest — both are fine; pick one per project
- - Model specs test validations, scopes, and methods; controller/request specs test HTTP behavior; system specs (Capybara) test critical user flows
- - FactoryBot for test data, not fixtures
- - `let` and `let!` for shared setup; `shared_examples` and `shared_context` for cross-spec reuse
- - Don't mock what you don't own — wrap third-party libraries in a thin adapter and mock the adapter
+ { enabled: enabled, attempts: attempts }
+ end
+ ```
- ## Project layout
+ This boundary expects string input for `attempts`, such as form/config text. Missing keys and malformed values remain explicit failures.
- - Standard Rails layout for Rails apps; `lib/` for code that's genuinely framework-free
- - Engines for in-app modular boundaries when the app grows large
- - Gems for code shared across multiple apps — extract sooner than feels comfortable
+ ## Checklist
- ## Common pitfalls
+ - Check Ruby/Rails version gates and retain the project's formatter and testing conventions.
+ - Distinguish nil/false/missing from invalid data; check mutation and block-return semantics.
+ - Verify transaction failure, database constraints, job duplication, and commit timing.
+ - Use installed project commands through `bundle exec` or binstubs when authorized; test observable behavior with the existing Minitest/RSpec setup. Do not replace fixtures or test libraries by preference.
- - `return` inside a block (not a lambda) raises `LocalJumpError` — use `next` to exit the iteration
- - `respond_to_missing?` must accompany every `method_missing` override
- - `require` searches `$LOAD_PATH`; `require_relative` is path-relative — mixing them up causes load-order bugs
- - `bundle exec` for any command that depends on the project's gems
+ ## References
+
+ - [Ruby release lines](https://www.ruby-lang.org/en/downloads/releases/)
+ - [Ruby 4.0 features and experimental status](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/)
+ - [Ruby 3.4 changes](https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/)
+ - [Data immutability](https://docs.ruby-lang.org/en/4.0/Data.html), [Proc control flow](https://docs.ruby-lang.org/en/4.0/Proc.html), and [Hash access](https://docs.ruby-lang.org/en/4.0/Hash.html)
+ - [Strong Parameters](https://api.rubyonrails.org/classes/ActionController/Parameters.html)
+ - [Active Record transactions](https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html)
+ - [Active Job and transaction behavior](https://guides.rubyonrails.org/active_job_basics.html)