소스 정보
- 저장소
- cosmix/loom
- 최근 소스 활동
- 2026년 9월 25일 06:00
- 감지된 SKILL.md 언어
- 영어
- 스타
- 61
- 포크
- 4
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- name
- loom-ruby
- description
- Ruby language expertise for idiomatic, production-quality code.
- triggers
- ["ruby","rb","bundler","gem","gemfile","gemspec","rake","rails","activerecord","activejob","rspec","minitest","rubocop","standardrb","sorbet","rbs","sidekiq","puma","sinatra","yjit"]
# Ruby Language Expertise
## Overview
Idiomatic, production-grade Ruby 3.x: Bundler, the Ruby 3 language additions, blocks and closures, Rails, and RSpec/Minitest. Assumes competence; the value is in what is easy to get wrong (keyword-argument separation, `proc` return semantics, `||=` on falsy values, N+1 queries, unsafe deserialization, the GVL) and in writing tests that loom's `rspec` and `minitest` adapters can select one at a time.
## Tooling
Preserve the repository's version manager and lockfile. The Ruby version lives in `.ruby-version` (read by rbenv, chruby, asdf, mise) and in the `Gemfile`'s `ruby` directive.
**Bundler** resolves dependencies and isolates the load path.
| Task | Command |
| --- | --- |
| Add a dependency | `bundle add faraday` (edits `Gemfile`, resolves, updates `Gemfile.lock`) |
| Add a dev/test dependency | `bundle add rspec --group "development, test"` |
| Install from the lock | `bundle install`; set `BUNDLE_FROZEN=true` in CI so drift fails the build |
| Update one gem | `bundle update --conservative faraday` (shared dependencies stay pinned) |
| Run inside the bundle | `bundle exec rspec`, or the binstubs `bin/rails`, `bin/rspec` |
| Known CVEs in the lock | `bundle exec bundler-audit check --update` |
| Deploy platforms | `bundle lock --add-platform x86_64-linux` |
- `bundle exec` activates exactly the locked versions; a bare `rspec` or `rake` may load a different installed version. `require "bundler/setup"` does the same from inside a script.
- A gem declares runtime dependencies in its `.gemspec`; its `Gemfile` holds `gemspec` plus development tools.
**Format and lint:** RuboCop (`rubocop -a` applies safe corrections; `-A` adds unsafe ones, so review that diff) or Standard (`standardrb --fix`, RuboCop with a fixed config). Rails 7.2+ generates `bin/rubocop` (rubocop-rails-omakase) and `bin/brakeman`. Plugins: `rubocop-rspec`, `rubocop-performance`, `rubocop-rails`. Disable a cop for a range with `# rubocop:disable Style/Foo` ... `# rubocop:enable Style/Foo`; never disable all cops for a file.
**Types (gradual, optional):** RBS signatures in `sig/*.rbs` checked by Steep, or Sorbet (`# typed: strict`, `sig { params(id: Integer).returns(String) }`, `srb tc`). Follow whichever the project uses; never add a second one.
**The gate:**
```bash
bundle exec rubocop # or: bundle exec standardrb
bundle exec srb tc # Sorbet projects only (Steep: bundle exec steep check)
bundle exec rspec # or: bin/rails test, bundle exec rake test
bundle exec brakeman --no-pager # Rails apps: static security scan
```
## Ruby 3 Language Features
`.ruby-version` bounds what you may use.
| Version | Feature |
| --- | --- |
| 3.0 | Keyword arguments separated from positional hashes; `case/in` pattern matching; endless methods `def sq(x) = x * x`; Ractor (experimental); fiber scheduler interface |
| 3.1 | Hash shorthand `{x:, y:}`; anonymous block forwarding `def f(&) = g(&)`; YJIT; Psych 4, so `YAML.load` is safe by default |
| 3.2 | `Data.define`; anonymous `*` and `**` forwarding; `Struct.new` accepts keyword arguments without `keyword_init: true`; `Fiber[]` storage |
| 3.3 | Prism parser shipped; YJIT speedups; M:N thread scheduler (opt-in) |
| 3.4 | `it` as the implicit block parameter; Prism is the default parser; string literals are "chilled" (mutation warns under `-W:deprecated`); `Hash#inspect` prints `{name: "x"}` |
**Keyword argument separation (3.0).** A hash passed positionally no longer becomes keywords, and keywords no longer collapse into a trailing positional hash. Code that relied on the 2.x conversion raises `ArgumentError`. Splat explicitly, and delegate with `...`.
```ruby
def connect(host, port: 5432, ssl: false) = open_socket(host, port, ssl)
opts = { port: 6543, ssl: true }
connect("db", opts) # ArgumentError on 3.x: opts is a second positional argument
connect("db", **opts) # keywords
def with_logging(...) = connect(...) # forwards positionals, keywords and the block
```
**Pattern matching.** `case/in` destructures arrays, hashes and any object implementing `deconstruct`/`deconstruct_keys`. With no matching branch and no `else` it raises `NoMatchingPatternError`, so an unhandled shape fails loudly. `expr => pattern` binds or raises; `expr in pattern` returns a boolean.
```ruby
case response
in { status: 200, body: { items: [_, *] => items } }
process(items)
in { status: 404 }
nil
in { status: 500.. => code }
raise UpstreamError, "upstream returned #{code}"
end
config => { database: { host: String => host } } # binds host; a missing key raises NoMatchingPatternKeyError, a non-String host NoMatchingPatternError
```
Hash patterns match a subset of keys (`**nil` forbids extra keys); array patterns match the whole array unless a `*rest` is present. Pin a local with `^expected`, an expression with `^(expr)`.
**Value objects with `Data.define` (3.2).** Immutable, keyword or positional construction, value equality, `with` for modified copies, pattern-matchable. Use it where older code used `Struct` for values that should never change; keep `Struct` for the rare case where mutability is the point.
```ruby
Point = Data.define(:x, :y) do
def +(other) = with(x: x + other.x, y: y + other.y)
end
origin = Point.new(x: 0, y: 0)
moved = origin.with(y: 5) # origin is unchanged
Point.new(1) # ArgumentError: missing keyword: :y
```
**Frozen string literals.** Put `# frozen_string_literal: true` at the top of every file (RuboCop enforces it): literals are frozen and deduplicated, which saves allocations and turns accidental mutation into `FrozenError`. Build strings from `+""` or `String.new(capacity: n)`; `str.dup` returns an unfrozen copy.
## Blocks, Procs and Lambdas
| | Block / `proc {}` | `lambda {}` / `->(x) {}` |
| --- | --- | --- |
| Arity | lenient: missing arguments are `nil`, extras dropped, a lone array auto-splats | strict: `ArgumentError` on mismatch |
| `return` | returns from the method that defined the block | returns from the lambda |
| `next` | ends this call of the block, with a value | same as `return` |
- A method takes a block implicitly (`yield`, `block_given?`) or explicitly (`&block`, which allocates a `Proc`; the anonymous `&` of 3.1 forwards without naming it).
- Return an `Enumerator` when no block is given, so callers can chain: `return enum_for(__method__) unless block_given?`.
- `&:name` calls `Symbol#to_proc`; `&method(:parse)` passes an existing method.
- Blocks capture local variables by reference: a block stored and called later sees the current value and can reassign it.
- `it` (3.4) and `_1` suit one-line blocks; name the parameter when a block spans lines.
- ⚠ `return` inside a `proc` that outlives its defining method raises `LocalJumpError`. Store callbacks as lambdas.
```ruby
def each_batch(size: 100)
return enum_for(__method__, size:) unless block_given?
offset = 0
loop do
rows = fetch(offset:, limit: size)
break if rows.empty?
yield rows
offset += size
end
end
each_batch.with_index { |rows, i| logger.info("batch #{i}: #{rows.size} rows") }
present = ->(value) { value.is_a?(String) && !value.empty? }
names.select(&present) # a lambda passed as the block
ids = users.map { it.id } # 3.4
```
## Errors and Exceptions
- Bare `rescue` and `rescue => e` catch `StandardError` only. Never `rescue Exception`: it also swallows `Interrupt`, `SystemExit` and `SignalException`, so Ctrl-C and `exit` stop working.
- Give a library one base error and a hierarchy under it. Callers rescue the base for everything or a subclass for one case.
- Raising inside a `rescue` sets `cause` automatically, so the original stays reachable as `e.cause`.
- Keep the `begin` body to the call that can fail. A method body is an implicit `begin`: `def x ... rescue ... ensure ... end`.
- `ensure` runs on every exit path. A `return` inside `ensure` discards the in-flight exception; never write one.
- `retry` re-runs the `begin` body; always bound it with a counter.
- ⚠ `Timeout.timeout` raises into the block from another thread at an arbitrary point, which can leave connections, locks and files half-updated. Use the library's own timeouts (`Net::HTTP#read_timeout`, driver `connect_timeout`, database `statement_timeout`).
```ruby
module Spool
class Error < StandardError; end
class SymlinkError < Error; end
class FetchError < Error; end
end
def fetch_with_retry(url, attempts: 3)
tries = 0
begin
tries += 1
http_get(url)
rescue Net::OpenTimeout, Errno::ECONNRESET
retry if tries < attempts
raise Spool::FetchError, "fetch failed after #{tries} attempts: #{url}" # original is #cause
end
end
```
## Concurrency
- **The GVL** lets one thread run Ruby code at a time (per Ractor). Threads help IO-bound work because blocking IO and `sleep` release the GVL; they give no speedup for CPU-bound Ruby. For CPU parallelism use processes (Puma workers, Sidekiq processes, `Process.fork`) or a native extension.
- **Puma** runs `workers` (processes) times `threads`. Class-level and global mutable state is shared by every thread in a worker: memoized class instance variables, `@@cache ||= {}`, a constant holding a mutable hash. Guard with `Mutex#synchronize`, use `Concurrent::Map` from concurrent-ruby, or keep the state per request.
- **Fibers:** under a fiber scheduler (the `async` gem, Falcon) stdlib IO, `sleep` and `Net::HTTP` yield instead of blocking, so one thread serves many IO-bound tasks.
- **Ractors** give real parallelism with isolated objects, but remain experimental and most gems are not Ractor-safe. Keep them out of application code.
- **Background jobs** go through ActiveJob backends (Sidekiq, Solid Queue, GoodJob). Arguments must be serializable primitives or GlobalID records, and every backend retries, so jobs must be idempotent.
```ruby
class RateCache
def initialize
@lock = Mutex.new
@rates = {}
end
def fetch(currency)
@lock.synchronize { @rates[currency] ||= load_rate(currency) } # check and set under one lock
end
end
```
## Rails Essentials
- **Autoloading (Zeitwerk):** paths must match constant names (`app/services/spool/writer.rb` defines `Spool::Writer`). Never `require` an autoloaded file; `bin/rails zeitwerk:check` validates the tree.
- **N+1 queries:** `includes` lets Rails choose, `preload` runs separate queries, `eager_load` uses one `LEFT JOIN` (needed to filter on the association). `strict_loading` on a model, association or relation raises on lazy loads; the `bullet` gem reports them in development.
- **Large tables:** `find_each` / `in_batches`; `Model.all.each` loads every row into memory.
- **Strong parameters:** `params.expect(user: [:name, :email])` (Rails 8) or `params.require(:user).permit(:name, :email)`. Never `permit!`, and never pass raw `params` to `create`/`update`.
- **Callbacks:** keep them to the model's own invariants (normalization, derived columns). `after_save` runs inside the transaction, so an email or job triggered there can run before the commit or for a row that is rolled back; use `after_commit` / `after_create_commit` or a service object for side effects.
- **Bypasses:** `update_column(s)`, `update_all`, `insert_all` and `delete_all` skip validations and callbacks. `validates :email, uniqueness: true` alone races; back it with a unique index.
- **Transactions:** `ActiveRecord::Base.transaction do ... end` rolls back on any exception; `raise ActiveRecord::Rollback` rolls back without propagating.
- **Secrets:** `bin/rails credentials:edit` (encrypted) or environment variables; never plaintext in the repository.
- **Rails 7.1+:** `normalizes`, `generates_token_for`, `authenticate_by` (timing-safe credential lookup). Rails 8 adds the authentication generator and Solid Queue/Cache/Cable defaults.
```ruby
class Order < ApplicationRecord
belongs_to :customer
has_many :line_items, dependent: :destroy
normalizes :reference, with: ->(ref) { ref.strip.upcase }
validates :reference, presence: true
after_create_commit -> { OrderMailer.confirmation(self).deliver_later }
scope :recent, -> { where(created_at: 7.days.ago..) }
end
Order.recent.includes(:customer, :line_items).find_each do |order|
export(order) # no per-row customer or line-item queries
end
```
## Testing
**RSpec.**
```ruby
# spec/spool/writer_spec.rb
RSpec.describe "Spool::Writer" do
subject(:writer) { Spool::Writer.new(root:) }
let(:root) { Pathname(Dir.mktmpdir) }
after { FileUtils.remove_entry(root) }
it "writes the payload into the spool directory" do
writer.write("job-1", "payload")
expect(root.join("job-1").read).to eq("payload")
end
it "rejects a symlinked spool directory" do
target = Pathname(Dir.mktmpdir)
link = root.join("spool")
File.symlink(target, link)
expect { Spool::Writer.new(root: link).write("job-1", "payload") }
.to raise_error(Spool::SymlinkError, /symlink/)
expect(target.join("job-1")).not_to exist
ensure
FileUtils.remove_entry(target) if target
end
end
```
- `let` is lazy and memoized per example; `let!` runs before every example. Prefer `let`; a `let!` that nothing reads is hidden setup.
- Verified doubles (`instance_double(Spool::Store, put: true)`) fail when the stubbed method does not exist on the real class or is called with the wrong arity; a plain `double` checks nothing.
- Always name the class in `raise_error`: a bare `raise_error` passes on the `NoMethodError` from a typo.
- `aggregate_failures` reports every failed expectation in an example instead of stopping at the first.
- Run in random order (`config.order = :random` plus `Kernel.srand config.seed`); reproduce with `--seed N`; `rspec --bisect` finds the minimal order-dependent pair.
GitHub에서 보기이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기