YARD is the documentation layer for public Ruby API; a sig is the type layer. Keep them separate: the signature states the types, the comment states the contract, the why, and the edge cases. A comment that cannot be maintained must be deleted — a stale comment is an active lie.
A fully documented public method: a why-summary, @param only where the name and sig fall short, the failure modes, and one runnable example.
# frozen_string_literal: true
# typed: strict
module Inventory
# Reserves `quantity` units of `sku` against the caller's order, holding them
# for {RESERVATION_TTL_SECONDS} so checkout can complete without a race.
#
# The hold is best-effort: stock can still sell out between reservation and
# capture, which is why {#capture_reservation} re-checks. Prefer this over
# decrementing stock directly — a raw decrement leaks units when an order is
# abandoned.
#
# @param quantity [Integer] units to hold; must be a positive whole-unit count
# @raise [OutOfStockError] fewer than `quantity` units are available — show the back-in-stock prompt
# @raise [SkuRetiredError] the SKU is no longer sold — drop it from the order
# @example
# hold = Inventory.reserve(order_id: order.id, sku: "SKU-1024", quantity: 2)
# Inventory.capture_reservation(hold.id)
sig { params(order_id: Integer, sku: Sku, quantity: Integer).returns(Reservation) }
def self.reserve(order_id:, sku:, quantity:)
# ...
end
endThe summary explains why a caller reaches for reserve over a raw decrement (14.3). @param appears only on quantity because that is the argument whose constraint — positive, whole-unit integer — the sig cannot capture (14.2). Both failure modes tell the caller what to do next (14.1). The @example is a real, runnable call site (14.5). The prose says nothing the sig already says — it adds the why and the edge case, nothing else.
Reasoning, step by step:
- The audience for a public symbol is a caller who will never open its source; they get the signature and the YARD hover-card. If the contract is absent from YARD, it does not exist for them.
- A not-externally-visible method whose name already carries the full meaning —
valid?,to_s, a two-line predicate — does not need prose that would only restate the name. Add YARD when a reader needs more than the name andsigto use the method safely. - A public class carries a class-level YARD comment: what it models, when to reach for it, and what invariants it guarantees. A
Data.definevalue object without a class comment leaves callers guessing at the fields' semantics.
Enforcement: review; every public method and class exported from the module surface carries a YARD block, verified during code review of new API additions.
Reasoning, step by step:
@param order_id [Integer] the order idadds nothing toorder_id: Integerin thesig; the name and the type already say it. Now there are two places to update on a rename.- A
sigenforced at runtime is a stronger statement than prose; prose that duplicates it is noise at best and a confident lie after a refactor at worst. - YARD earns its line by adding what the
sigcannot: intent, constraints, units, valid ranges, the meaning of a sentinel, an edge case the type system cannot express. "Must be a positive whole-unit count" adds information; "an Integer" does not.
Enforcement: review; reject @param and @return lines that only echo the type; keep the ones that add a constraint, unit, or sentinel.
Reasoning, step by step:
# increment retry counteraboveretries += 1is dead weight — the line says that already.# back off only on 5xx; 4xx will never succeed on retrycarries a decision the code cannot express.- The what is verifiable by reading the code; the why is not. Spend comment budget on the invisible part: the constraint that forced this shape, the bug this guards against, the spec clause behind the magic constant.
- Assume the reader knows Ruby but not your domain and not your intent. They can parse
line_items.select(&:taxable?)without a comment; they cannot know that non-taxable items are still included in the fulfillment fee unless you say so.
# Select only taxable items for the sales-tax line; the fulfillment fee applies
# to all line items and is computed separately in FulfillmentCalculator.
taxable = order.line_items.select(&:taxable?)Enforcement: review; delete mechanics-narrating comments on sight, keep the ones a reviewer could not have inferred from the diff.
14.4 — A file or class header states what it is and how to use it; a file with zero or more than one class gets a top-of-file comment.
Reasoning, step by step:
- A module file with multiple classes or no class at all gives a reader no entry point without a file-level comment. The comment is the map before the territory.
- A class-level header states the model: what the class represents, its invariants, and the pattern a caller uses. A
Customerclass without a header leaves callers wondering whether it is a value object, a service object, or an ActiveRecord subclass. - Airbnb's guide mandates a top-of-file comment when a file contains zero or more than one class. Follow that rule exactly; a single-class file where the class header comment already says everything is exempt from a separate file-level comment.
# frozen_string_literal: true
# typed: strict
# Utilities for normalising and validating Money amounts at order ingestion.
# All methods are pure functions: no I/O, no mutation of arguments.
# Use {MoneyParser.parse} at every inbound boundary before arithmetic.
module MoneyUtils
# ...
endEnforcement: rubocop-airbnb; review confirms the comment addresses what the file is and how to use it, not just what it contains.
Reasoning, step by step:
- A signature shows the shape of a call but not the shape of usage — the order of operations, what to do with the return, which helper pairs with it. One worked example answers all three faster than prose.
- A runnable example is the fastest thing a caller reads and the hardest thing to misread; prose describing the same call leaves room for ambiguity the code does not.
- Obvious one-liners — a pure
Money.zeroor a predicateorder.paid?— do not need an example; the signature is the example. Reserve@examplefor APIs with non-obvious sequencing, setup, or pairing.
# @example
# result = OrderPricer.price(
# order: order,
# customer: customer,
# effective_at: Time.now.utc,
# )
# puts result.total_centsEnforcement: review; non-obvious public methods carry a realistic @example that reflects the current API.
Reasoning, step by step:
- An anonymous
# TODOis a wish nobody owns.# TODO(Lena Schmidt): remove after SKU normaliser ships in v4is a task with a name, a reason, and enough context to evaluate later. - The full name makes the comment
grep-able by person:grep -r "TODO(Lena" .finds every outstanding item in one command. A first name alone or a username alias breaks across team members. - The explanation is mandatory: "why does this debt exist" and "what removes it" must both fit in the comment. A TODO without an explanation is a note that says "fix this" and nothing more — insufficient to act on or to decide whether it is still relevant.
# TODO(Omar Mazari): replace Money#cents with MoneyV2#minor_units once
# the currency-migration rake task has been run in production (see ADR-0042).Enforcement: review; rubocop custom cop or grep in CI rejects # TODO not matching # TODO\([^)]+\): .+.
Reasoning, step by step:
- Commented-out code is the loudest kind of clutter: it looks like it might matter, forces every reader to parse it and decide whether to restore it, and provides no explanation of why it was removed.
- Version control remembers every line ever committed.
git log -S 'removed_method'recovers it in seconds. The comment buys nothing thatgitdoes not already provide — andgitdoes not mislead. - If the code must be recoverable for a specific reason, the reason belongs in a commit message or an ADR, not in a comment block.
Enforcement: rubocop Style/CommentedKeyword and review; commented-out code is rejected at review regardless of apparent intent.
Reasoning, step by step:
=begin/=endblock comments cannot be indented — they must start in column 0. Inside a method or class body they look like a syntax error to anyone who has not memorised the rule.#line comments work at any indentation level, are easy to spot withgrep, and compose naturally with the rest of the file. There is no advantage=begin/=endholds that YARD or#lines do not already cover.- YARD uses
#comment lines for documentation blocks. Mixing block-comment syntax into a codebase that uses YARD creates two documentation dialects with no benefit.
Enforcement: rubocop Style/BlockComments; CI rejects any =begin outside an intentional literal-string test fixture.
Reasoning, step by step:
- A reader trusts a comment more than they trust their own reading of the code. A wrong comment spends that trust to point them at the wrong conclusion; deleting it strictly improves the file.
- Documentation drifts only when a change updates the code and leaves the prose behind. The gap costs nothing to close at edit time and is expensive forever after, once nobody remembers which of the two is right.
- "I'll update the comment later" is technical debt that never gets paid. The discipline is one commit: code and its comment move together, or not at all. This is root rule 12 — zero debt — applied to prose.
Enforcement: review; a diff that changes behaviour and leaves surrounding YARD or inline comments stale does not merge — update or delete in the same commit.
Reasoning, step by step:
- A comment that reads like a headline ("Retry logic") gives less information than a sentence ("Retry up to three times with exponential back-off; give up on the fourth failure and raise."). Sentences force completeness.
- Proper capitalisation and ending punctuation signal that the comment is finished prose, not a placeholder. A fragment like
# handles nillooks like a stub;# Returns zero when the order has no line items.reads as intentional documentation. - The single exception is a short trailing note on the same line as code —
total = subtotal + tax # cents— where a full sentence would overflow the 100-column limit. That note does not need terminal punctuation.
# Bad — fragment, no punctuation, looks like a stub.
# check for abandoned carts
# Good — sentence, tells the reader exactly what and why.
# Skip orders with no activity in the last 30 days; the warehouse
# considers them abandoned and has already released their stock holds.
order.line_items.select { |li| li.updated_at > 30.days.ago }Enforcement: review; YARD blocks and standalone inline comments are complete sentences; trailing same-line notes are exempt from the sentence requirement.
- Sorbet
sigblocks that replace type prose in YARD: 03-type-safety-and-nil-discipline.md. - Naming as the first line of documentation — predicate
?, bang!, effect-verb discipline: 02-naming-conventions.md. - Public method surface and the minimal API contract that YARD documents: 10-api-design.md.
@raiseand theStandardErrorsubclass hierarchy YARD references: 08-error-handling.md.- One class per file and Zeitwerk module layout that the file-header rule (14.4) assumes: 12-module-organization.md.
- Formatting rules — 100-col limit, double quotes, 2-space indent — that apply inside YARD fences and examples: 01-formatting-and-tooling.md.