[2.9.0] Rename plugin to compound-engineering

BREAKING: Plugin renamed from compounding-engineering to compound-engineering.
Users will need to reinstall with the new name:

  claude /plugin install compound-engineering

Changes:
- Renamed plugin directory and all references
- Updated documentation counts (24 agents, 19 commands)
- Added julik-frontend-races-reviewer to docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kieran Klaassen
2025-12-02 17:32:04 -08:00
parent 4b49e5344d
commit 6c5b3e40db
121 changed files with 136 additions and 117 deletions

View File

@@ -0,0 +1,699 @@
# DHH Ruby/Rails Patterns Reference
Comprehensive code patterns extracted from 37signals' Campfire codebase and DHH's public teachings.
## Controller Patterns
### REST-Pure Controller Design
DHH's controller philosophy is "fundamentalistic" about REST. Every controller maps to a resource with only the 7 standard actions.
```ruby
# ✅ CORRECT: Standard REST actions only
class MessagesController < ApplicationController
def index; end
def show; end
def new; end
def create; end
def edit; end
def update; end
def destroy; end
end
# ❌ WRONG: Custom actions
class MessagesController < ApplicationController
def archive # NO
def unarchive # NO
def search # NO
def drafts # NO
end
# ✅ CORRECT: New controllers for custom behavior
class Messages::ArchivesController < ApplicationController
def create # archives a message
def destroy # unarchives a message
end
class Messages::DraftsController < ApplicationController
def index # lists drafts
end
class Messages::SearchesController < ApplicationController
def show # shows search results
end
```
### Controller Concerns for Shared Behavior
```ruby
# app/controllers/concerns/room_scoped.rb
module RoomScoped
extend ActiveSupport::Concern
included do
before_action :set_room
end
private
def set_room
@room = Current.user.rooms.find(params[:room_id])
end
end
# Usage
class MessagesController < ApplicationController
include RoomScoped
end
```
### Complete Controller Example
```ruby
class MessagesController < ApplicationController
include ActiveStorage::SetCurrent, RoomScoped
before_action :set_room, except: :create
before_action :set_message, only: %i[ show edit update destroy ]
before_action :ensure_can_administer, only: %i[ edit update destroy ]
layout false, only: :index
def index
@messages = find_paged_messages
if @messages.any?
fresh_when @messages
else
head :no_content
end
end
def create
set_room
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
deliver_webhooks_to_bots
rescue ActiveRecord::RecordNotFound
render action: :room_not_found
end
def show
end
def edit
end
def update
@message.update!(message_params)
@message.broadcast_replace_to @room, :messages,
target: [ @message, :presentation ],
partial: "messages/presentation",
attributes: { maintain_scroll: true }
redirect_to room_message_url(@room, @message)
end
def destroy
@message.destroy
@message.broadcast_remove_to @room, :messages
end
private
def set_message
@message = @room.messages.find(params[:id])
end
def ensure_can_administer
head :forbidden unless Current.user.can_administer?(@message)
end
def find_paged_messages
case
when params[:before].present?
@room.messages.with_creator.page_before(@room.messages.find(params[:before]))
when params[:after].present?
@room.messages.with_creator.page_after(@room.messages.find(params[:after]))
else
@room.messages.with_creator.last_page
end
end
def message_params
params.require(:message).permit(:body, :attachment, :client_message_id)
end
def deliver_webhooks_to_bots
bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) }
end
def bots_eligible_for_webhook
@room.direct? ? @room.users.active_bots : @message.mentionees.active_bots
end
end
```
## Model Patterns
### Semantic Association Naming
```ruby
class Message < ApplicationRecord
# ✅ Semantic names that express domain concepts
belongs_to :creator, class_name: "User"
belongs_to :room
has_many :mentions
has_many :mentionees, through: :mentions, source: :user
# ❌ Generic names
belongs_to :user # Too generic - creator is clearer
end
class Room < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
has_many :messages, dependent: :destroy
# Semantic scope
scope :direct, -> { where(direct: true) }
def direct?
direct
end
end
```
### Scope Design
```ruby
class Message < ApplicationRecord
# Eager loading scopes
scope :with_creator, -> { includes(:creator) }
scope :with_attachments, -> { includes(attachment_attachment: :blob) }
# Cursor-based pagination scopes
scope :page_before, ->(cursor) {
where("id < ?", cursor.id).order(id: :desc).limit(50)
}
scope :page_after, ->(cursor) {
where("id > ?", cursor.id).order(id: :asc).limit(50)
}
scope :last_page, -> { order(id: :desc).limit(50) }
# Status scopes as chainable lambdas
scope :recent, -> { where("created_at > ?", 24.hours.ago) }
scope :pinned, -> { where(pinned: true) }
end
```
### Custom Creation Methods
```ruby
class Message < ApplicationRecord
def self.create_with_attachment!(params)
transaction do
message = create!(params.except(:attachment))
message.attach_file(params[:attachment]) if params[:attachment].present?
message
end
end
def attach_file(attachment)
file.attach(attachment)
update!(has_attachment: true)
end
end
```
### Authorization on Models
```ruby
class User < ApplicationRecord
def can_administer?(message)
message.creator == self || admin?
end
def can_access?(room)
rooms.include?(room) || admin?
end
def can_invite_to?(room)
room.creator == self || admin?
end
end
# Usage in controller
def ensure_can_administer
head :forbidden unless Current.user.can_administer?(@message)
end
```
### Model Broadcasting
```ruby
class Message < ApplicationRecord
after_create_commit :broadcast_create
after_update_commit :broadcast_update
after_destroy_commit :broadcast_destroy
def broadcast_create
broadcast_append_to room, :messages,
target: "messages",
partial: "messages/message"
end
def broadcast_update
broadcast_replace_to room, :messages,
target: dom_id(self, :presentation),
partial: "messages/presentation"
end
def broadcast_destroy
broadcast_remove_to room, :messages
end
end
```
## Current Attributes Pattern
### Definition
```ruby
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user
attribute :session
attribute :request_id
attribute :user_agent
resets { Time.zone = nil }
def user=(user)
super
Time.zone = user&.time_zone
end
end
```
### Setting in Controller
```ruby
class ApplicationController < ActionController::Base
before_action :set_current_attributes
private
def set_current_attributes
Current.user = authenticate_user
Current.session = session
Current.request_id = request.request_id
Current.user_agent = request.user_agent
end
end
```
### Usage Throughout App
```ruby
# In models
class Message < ApplicationRecord
before_create :set_creator
private
def set_creator
self.creator ||= Current.user
end
end
# In views
<%= Current.user.name %>
# In jobs
class NotificationJob < ApplicationJob
def perform(message)
# Current is reset in jobs - pass what you need
message.room.users.each { |user| notify(user, message) }
end
end
```
## Ruby Idioms
### Guard Clauses Over Nested Conditionals
```ruby
# ✅ Guard clauses
def process_message
return unless message.valid?
return if message.spam?
return unless Current.user.can_access?(message.room)
message.deliver
end
# ❌ Nested conditionals
def process_message
if message.valid?
unless message.spam?
if Current.user.can_access?(message.room)
message.deliver
end
end
end
end
```
### Expression-less Case Statements
```ruby
# ✅ Clean case without expression
def status_class
case
when urgent? then "bg-red"
when pending? then "bg-yellow"
when completed? then "bg-green"
else "bg-gray"
end
end
# For routing/dispatch logic
def find_paged_messages
case
when params[:before].present?
messages.page_before(params[:before])
when params[:after].present?
messages.page_after(params[:after])
else
messages.last_page
end
end
```
### Method Chaining
```ruby
# ✅ Fluent, chainable API
@room.messages
.with_creator
.with_attachments
.excluding(@message.creator)
.page_before(cursor)
# On collections
bots_eligible_for_webhook
.excluding(@message.creator)
.each { |bot| bot.deliver_webhook_later(@message) }
```
### Implicit Returns
```ruby
# ✅ Implicit return - the Ruby way
def full_name
"#{first_name} #{last_name}"
end
def can_administer?(message)
message.creator == self || admin?
end
# ❌ Explicit return (only when needed for early exit)
def full_name
return "#{first_name} #{last_name}" # Unnecessary
end
```
## View Patterns
### Helper Methods for Complex HTML
```ruby
# app/helpers/messages_helper.rb
module MessagesHelper
def message_container(message, &block)
tag.div(
id: dom_id(message),
class: message_classes(message),
data: {
controller: "message",
message_id_value: message.id,
action: "click->message#select"
},
&block
)
end
private
def message_classes(message)
classes = ["message"]
classes << "message--mine" if message.creator == Current.user
classes << "message--highlighted" if message.highlighted?
classes.join(" ")
end
end
```
### Turbo Frame Patterns
```erb
<%# app/views/messages/index.html.erb %>
<%= turbo_frame_tag "messages", data: { turbo_action: "advance" } do %>
<%= render @messages %>
<% if @messages.any? %>
<%= link_to "Load more",
room_messages_path(@room, before: @messages.last.id),
data: { turbo_frame: "messages" } %>
<% end %>
<% end %>
```
### Stimulus Controller Integration
```erb
<div data-controller="message-form"
data-message-form-submit-url-value="<%= room_messages_path(@room) %>">
<%= form_with model: [@room, Message.new],
data: { action: "submit->message-form#submit" } do |f| %>
<%= f.text_area :body,
data: { action: "keydown.enter->message-form#submitOnEnter" } %>
<%= f.submit "Send" %>
<% end %>
</div>
```
## Testing Patterns
### System Tests First
```ruby
# test/system/messages_test.rb
class MessagesTest < ApplicationSystemTestCase
test "sending a message" do
sign_in users(:david)
visit room_path(rooms(:watercooler))
fill_in "Message", with: "Hello, world!"
click_button "Send"
assert_text "Hello, world!"
end
test "editing own message" do
sign_in users(:david)
visit room_path(rooms(:watercooler))
within "#message_#{messages(:greeting).id}" do
click_on "Edit"
end
fill_in "Message", with: "Updated message"
click_button "Save"
assert_text "Updated message"
end
end
```
### Fixtures Over Factories
```yaml
# test/fixtures/users.yml
david:
name: David
email: david@example.com
admin: true
jason:
name: Jason
email: jason@example.com
admin: false
# test/fixtures/rooms.yml
watercooler:
name: Water Cooler
creator: david
direct: false
# test/fixtures/messages.yml
greeting:
body: Hello everyone!
room: watercooler
creator: david
```
### Integration Tests for API
```ruby
# test/integration/messages_api_test.rb
class MessagesApiTest < ActionDispatch::IntegrationTest
test "creating a message via API" do
post room_messages_url(rooms(:watercooler)),
params: { message: { body: "API message" } },
headers: auth_headers(users(:david))
assert_response :success
assert Message.exists?(body: "API message")
end
end
```
## Configuration Patterns
### Solid Queue Setup
```ruby
# config/queue.yml
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 5
processes: 1
polling_interval: 0.1
development:
<<: *default
production:
<<: *default
workers:
- queues: "*"
threads: 10
processes: 2
```
### Database Configuration for SQLite
```ruby
# config/database.yml
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: storage/development.sqlite3
production:
<<: *default
database: storage/production.sqlite3
```
### Single Container Deployment
```dockerfile
# Dockerfile
FROM ruby:3.3
RUN apt-get update && apt-get install -y \
libsqlite3-dev \
libvips \
ffmpeg
WORKDIR /rails
COPY . .
RUN bundle install
RUN rails assets:precompile
EXPOSE 80 443
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
```
## Anti-Patterns to Avoid
### Don't Add Service Objects for Simple Cases
```ruby
# ❌ Over-abstraction
class MessageCreationService
def initialize(room, params, user)
@room = room
@params = params
@user = user
end
def call
message = @room.messages.build(@params)
message.creator = @user
message.save!
BroadcastService.new(message).call
message
end
end
# ✅ Keep it in the model
class Message < ApplicationRecord
def self.create_with_broadcast!(params)
create!(params).tap(&:broadcast_create)
end
end
```
### Don't Use Policy Objects for Simple Auth
```ruby
# ❌ Separate policy class
class MessagePolicy
def initialize(user, message)
@user = user
@message = message
end
def update?
@message.creator == @user || @user.admin?
end
end
# ✅ Method on User model
class User < ApplicationRecord
def can_administer?(message)
message.creator == self || admin?
end
end
```
### Don't Mock Everything
```ruby
# ❌ Over-mocked test
test "sending message" do
room = mock("room")
user = mock("user")
message = mock("message")
room.expects(:messages).returns(stub(create!: message))
message.expects(:broadcast_create)
MessagesController.new.create
end
# ✅ Test the real thing
test "sending message" do
sign_in users(:david)
post room_messages_url(rooms(:watercooler)),
params: { message: { body: "Hello" } }
assert_response :success
assert Message.exists?(body: "Hello")
end
```

View File

@@ -0,0 +1,179 @@
# DHH Ruby Style Resources
Links to source material, documentation, and further reading for mastering DHH's Ruby/Rails style.
## Primary Source Code
### Campfire (Once)
The main codebase this style guide is derived from.
- **Repository**: https://github.com/basecamp/once-campfire
- **Messages Controller**: https://github.com/basecamp/once-campfire/blob/main/app/controllers/messages_controller.rb
- **JavaScript/Stimulus**: https://github.com/basecamp/once-campfire/tree/main/app/javascript
- **Deployment**: Single Docker container with SQLite
### Other 37signals Open Source
- **Solid Queue**: https://github.com/rails/solid_queue - Database-backed Active Job backend
- **Solid Cache**: https://github.com/rails/solid_cache - Database-backed Rails cache
- **Solid Cable**: https://github.com/rails/solid_cable - Database-backed Action Cable adapter
- **Kamal**: https://github.com/basecamp/kamal - Zero-downtime deployment tool
- **Turbo**: https://github.com/hotwired/turbo-rails - Hotwire's SPA-like page accelerator
- **Stimulus**: https://github.com/hotwired/stimulus - Modest JavaScript framework
## Articles & Blog Posts
### Controller Organization
- **How DHH Organizes His Rails Controllers**: https://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
- Definitive article on REST-pure controller design
- Documents the "only 7 actions" philosophy
- Shows how to create new controllers instead of custom actions
### Testing Philosophy
- **37signals Dev - Pending Tests**: https://dev.37signals.com/pending-tests/
- How 37signals handles incomplete tests
- Pragmatic approach to test coverage
- **37signals Dev - All About QA**: https://dev.37signals.com/all-about-qa/
- QA philosophy at 37signals
- Balance between automated and manual testing
### Architecture & Deployment
- **Deploy Campfire on Railway**: https://railway.com/deploy/campfire
- Single-container deployment example
- SQLite in production patterns
## Official Documentation
### Rails Guides (DHH's Vision)
- **Rails Doctrine**: https://rubyonrails.org/doctrine
- The philosophical foundation
- Convention over configuration explained
- "Optimize for programmer happiness"
### Hotwire
- **Hotwire**: https://hotwired.dev/
- Official Hotwire documentation
- Turbo Drive, Frames, and Streams
- **Turbo Handbook**: https://turbo.hotwired.dev/handbook/introduction
- **Stimulus Handbook**: https://stimulus.hotwired.dev/handbook/introduction
### Current Attributes
- **Rails API - CurrentAttributes**: https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
- Official documentation for the Current pattern
- Thread-isolated attribute singleton
## Videos & Talks
### DHH Keynotes
- **RailsConf Keynotes**: Search YouTube for "DHH RailsConf"
- Annual state of Rails addresses
- Philosophy and direction discussions
### Hotwire Tutorials
- **Hotwire Demo by DHH**: Original demo showing the approach
- **GoRails Hotwire Series**: Practical implementation tutorials
## Books
### By DHH & 37signals
- **Getting Real**: https://basecamp.com/gettingreal
- Product development philosophy
- Less is more approach
- **Remote**: Working remotely philosophy
- **It Doesn't Have to Be Crazy at Work**: Calm company culture
### Rails Books
- **Agile Web Development with Rails**: The original Rails book
- **The Rails Way**: Comprehensive Rails patterns
## Gems & Tools Used
### Core Stack
```ruby
# Gemfile patterns from Campfire
gem "rails", "~> 8.0"
gem "sqlite3"
gem "propshaft" # Asset pipeline
gem "importmap-rails" # JavaScript imports
gem "turbo-rails" # Hotwire Turbo
gem "stimulus-rails" # Hotwire Stimulus
gem "solid_queue" # Job backend
gem "solid_cache" # Cache backend
gem "solid_cable" # WebSocket backend
gem "kamal" # Deployment
gem "thruster" # HTTP/2 proxy
gem "image_processing" # Active Storage variants
```
### Development
```ruby
group :development do
gem "web-console"
gem "rubocop-rails-omakase" # 37signals style rules
end
group :test do
gem "capybara"
gem "selenium-webdriver"
end
```
## RuboCop Configuration
37signals publishes their RuboCop rules:
- **rubocop-rails-omakase**: https://github.com/rails/rubocop-rails-omakase
- Official Rails/37signals style rules
- Use this for consistent style enforcement
```yaml
# .rubocop.yml
inherit_gem:
rubocop-rails-omakase: rubocop.yml
# Project-specific overrides if needed
```
## Community Resources
### Forums & Discussion
- **Ruby on Rails Discourse**: https://discuss.rubyonrails.org/
- **Reddit r/rails**: https://reddit.com/r/rails
### Podcasts
- **Remote Ruby**: Ruby/Rails discussions
- **Ruby Rogues**: Long-running Ruby podcast
- **The Bike Shed**: Thoughtbot's development podcast
## Key Philosophy Documents
### The Rails Doctrine Pillars
1. Optimize for programmer happiness
2. Convention over Configuration
3. The menu is omakase
4. No one paradigm
5. Exalt beautiful code
6. Provide sharp knives
7. Value integrated systems
8. Progress over stability
9. Push up a big tent
### DHH Quotes to Remember
> "The vast majority of Rails controllers can use the same seven actions."
> "If you're adding a custom action, you're probably missing a controller."
> "Clear code is better than clever code."
> "The test file should be a love letter to the code."
> "SQLite is enough for most applications."
## Version History
This style guide is based on:
- Campfire source code (2024)
- Rails 8.0 conventions
- Ruby 3.3 syntax preferences
- Hotwire 2.0 patterns
Last updated: 2024