+```
+
+**Helper extraction** - shared utilities in separate modules:
+```javascript
+// app/javascript/helpers/timing.js
+export function debounce(fn, delay) {
+ let timeout
+ return (...args) => {
+ clearTimeout(timeout)
+ timeout = setTimeout(() => fn(...args), delay)
+ }
+}
+```
+
+**Event dispatching** for loose coupling:
+```javascript
+this.dispatch("selected", { detail: { id: this.idValue } })
+```
+
+
+
+## View Helpers (Stimulus-Integrated)
+
+**Dialog helper:**
+```ruby
+def dialog_tag(id, &block)
+ tag.dialog(
+ id: id,
+ data: {
+ controller: "dialog",
+ action: "click->dialog#clickOutside keydown.esc->dialog#close"
+ },
+ &block
+ )
+end
+```
+
+**Auto-submit form helper:**
+```ruby
+def auto_submit_form_with(model:, delay: 300, **options, &block)
+ form_with(
+ model: model,
+ data: {
+ controller: "auto-submit",
+ auto_submit_delay_value: delay,
+ action: "input->auto-submit#submit"
+ },
+ **options,
+ &block
+ )
+end
+```
+
+**Copy button helper:**
+```ruby
+def copy_button(content:, label: "Copy")
+ tag.button(
+ label,
+ data: {
+ controller: "copy",
+ copy_content_value: content,
+ action: "click->copy#copy"
+ }
+ )
+end
+```
+
+
## CSS Architecture
@@ -205,3 +432,79 @@ Vanilla CSS with modern features, no preprocessors.
.card.closed { }
```
+
+
+## User-Specific Content in Caches
+
+Move personalization to client-side JavaScript to preserve caching:
+
+```erb
+<%# Cacheable fragment %>
+<% cache card do %>
+
+
+
+<% end %>
+```
+
+```javascript
+// Reveal user-specific elements after cache hit
+export default class extends Controller {
+ static values = { currentUser: Number }
+ static targets = ["ownerOnly"]
+
+ connect() {
+ const creatorId = parseInt(this.element.dataset.creatorId)
+ if (creatorId === this.currentUserValue) {
+ this.ownerOnlyTargets.forEach(el => el.classList.remove("hidden"))
+ }
+ }
+}
+```
+
+**Extract dynamic content** to separate frames:
+```erb
+<% cache [card, board] do %>
+
+ <%= turbo_frame_tag card, :assignment,
+ src: card_assignment_path(card),
+ refresh: :morph %>
+
+<% end %>
+```
+
+Assignment dropdown updates independently without invalidating parent cache.
+
+
+
+## Broadcasting with Turbo Streams
+
+**Model callbacks** for real-time updates:
+```ruby
+class Card < ApplicationRecord
+ include Broadcastable
+
+ after_create_commit :broadcast_created
+ after_update_commit :broadcast_updated
+ after_destroy_commit :broadcast_removed
+
+ private
+ def broadcast_created
+ broadcast_append_to [Current.account, board], :cards
+ end
+
+ def broadcast_updated
+ broadcast_replace_to [Current.account, board], :cards
+ end
+
+ def broadcast_removed
+ broadcast_remove_to [Current.account, board], :cards
+ end
+end
+```
+
+**Scope by tenant** using `[Current.account, resource]` pattern.
+
diff --git a/plugins/compound-engineering/skills/dhh-rails-style/references/gems.md b/plugins/compound-engineering/skills/dhh-rails-style/references/gems.md
index 86b6576..00933b9 100644
--- a/plugins/compound-engineering/skills/dhh-rails-style/references/gems.md
+++ b/plugins/compound-engineering/skills/dhh-rails-style/references/gems.md
@@ -89,8 +89,107 @@ Why: REST is sufficient when you control both ends. GraphQL complexity not justi
factory_bot → Fixtures
```
Why: Fixtures are simpler, faster, and encourage thinking about data relationships upfront.
+
+**Service Objects:**
+```
+Interactor, Trailblazer → Fat models
+```
+Why: Business logic stays in models. Methods like `card.close` instead of `CardCloser.call(card)`.
+
+**Form Objects:**
+```
+Reform, dry-validation → params.expect + model validations
+```
+Why: Rails 7.1's `params.expect` is clean enough. Contextual validations on model.
+
+**Decorators:**
+```
+Draper → View helpers + partials
+```
+Why: Helpers and partials are simpler. No decorator indirection.
+
+**CSS:**
+```
+Tailwind, Sass → Native CSS
+```
+Why: Modern CSS has nesting, variables, layers. No build step needed.
+
+**Frontend:**
+```
+React, Vue, SPAs → Turbo + Stimulus
+```
+Why: Server-rendered HTML with sprinkles of JS. SPA complexity not justified.
+
+**Testing:**
+```
+RSpec → Minitest
+```
+Why: Simpler, faster boot, less DSL magic, ships with Rails.
+
+## Testing Philosophy
+
+**Minitest** - simpler, faster:
+```ruby
+class CardTest < ActiveSupport::TestCase
+ test "closing creates closure" do
+ card = cards(:one)
+ assert_difference -> { Card::Closure.count } do
+ card.close
+ end
+ assert card.closed?
+ end
+end
+```
+
+**Fixtures** - loaded once, deterministic:
+```yaml
+# test/fixtures/cards.yml
+open_card:
+ title: Open Card
+ board: main
+ creator: alice
+
+closed_card:
+ title: Closed Card
+ board: main
+ creator: bob
+```
+
+**Dynamic timestamps** with ERB:
+```yaml
+recent:
+ title: Recent
+ created_at: <%= 1.hour.ago %>
+
+old:
+ title: Old
+ created_at: <%= 1.month.ago %>
+```
+
+**Time travel** for time-dependent tests:
+```ruby
+test "expires after 15 minutes" do
+ magic_link = MagicLink.create!(user: users(:alice))
+
+ travel 16.minutes
+
+ assert magic_link.expired?
+end
+```
+
+**VCR** for external APIs:
+```ruby
+VCR.use_cassette("stripe/charge") do
+ charge = Stripe::Charge.create(amount: 1000)
+ assert charge.paid
+end
+```
+
+**Tests ship with features** - same commit, not before or after.
+
+
## Decision Framework
diff --git a/plugins/compound-engineering/skills/dhh-rails-style/references/models.md b/plugins/compound-engineering/skills/dhh-rails-style/references/models.md
index 6df1bb5..4a8a15d 100644
--- a/plugins/compound-engineering/skills/dhh-rails-style/references/models.md
+++ b/plugins/compound-engineering/skills/dhh-rails-style/references/models.md
@@ -212,3 +212,148 @@ card.close
card.ungild
```
+
+
+## Validation Philosophy
+
+Minimal validations on models. Use contextual validations on form/operation objects:
+
+```ruby
+# Model - minimal
+class User < ApplicationRecord
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
+end
+
+# Form object - contextual
+class Signup
+ include ActiveModel::Model
+
+ attr_accessor :email, :name, :terms_accepted
+
+ validates :email, :name, presence: true
+ validates :terms_accepted, acceptance: true
+
+ def save
+ return false unless valid?
+ User.create!(email: email, name: name)
+ end
+end
+```
+
+**Prefer database constraints** over model validations for data integrity:
+```ruby
+# migration
+add_index :users, :email, unique: true
+add_foreign_key :cards, :boards
+```
+
+
+
+## Let It Crash Philosophy
+
+Use bang methods that raise exceptions on failure:
+
+```ruby
+# Preferred - raises on failure
+@card = Card.create!(card_params)
+@card.update!(title: new_title)
+@comment.destroy!
+
+# Avoid - silent failures
+@card = Card.create(card_params) # returns false on failure
+if @card.save
+ # ...
+end
+```
+
+Let errors propagate naturally. Rails handles ActiveRecord::RecordInvalid with 422 responses.
+
+
+
+## Default Values with Lambdas
+
+Use lambda defaults for associations with Current:
+
+```ruby
+class Card < ApplicationRecord
+ belongs_to :creator, class_name: "User", default: -> { Current.user }
+ belongs_to :account, default: -> { Current.account }
+end
+
+class Comment < ApplicationRecord
+ belongs_to :commenter, class_name: "User", default: -> { Current.user }
+end
+```
+
+Lambdas ensure dynamic resolution at creation time.
+
+
+
+## Rails 7.1+ Model Patterns
+
+**Normalizes** - clean data before validation:
+```ruby
+class User < ApplicationRecord
+ normalizes :email, with: ->(email) { email.strip.downcase }
+ normalizes :phone, with: ->(phone) { phone.gsub(/\D/, "") }
+end
+```
+
+**Delegated Types** - replace polymorphic associations:
+```ruby
+class Message < ApplicationRecord
+ delegated_type :messageable, types: %w[Comment Reply Announcement]
+end
+
+# Now you get:
+message.comment? # true if Comment
+message.comment # returns the Comment
+Message.comments # scope for Comment messages
+```
+
+**Store Accessor** - structured JSON storage:
+```ruby
+class User < ApplicationRecord
+ store :settings, accessors: [:theme, :notifications_enabled], coder: JSON
+end
+
+user.theme = "dark"
+user.notifications_enabled = true
+```
+
+
+
+## Concern Guidelines
+
+- **50-150 lines** per concern (most are ~100)
+- **Cohesive** - related functionality only
+- **Named for capabilities** - `Closeable`, `Watchable`, not `CardHelpers`
+- **Self-contained** - associations, scopes, methods together
+- **Not for mere organization** - create when genuine reuse needed
+
+**Touch chains** for cache invalidation:
+```ruby
+class Comment < ApplicationRecord
+ belongs_to :card, touch: true
+end
+
+class Card < ApplicationRecord
+ belongs_to :board, touch: true
+end
+```
+
+When comment updates, card's `updated_at` changes, which cascades to board.
+
+**Transaction wrapping** for related updates:
+```ruby
+class Card < ApplicationRecord
+ def close(creator: Current.user)
+ transaction do
+ create_closure!(creator: creator)
+ record_event(:closed)
+ notify_watchers_later
+ end
+ end
+end
+```
+
diff --git a/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md b/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md
index 5e3ea62..be6b238 100644
--- a/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md
+++ b/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md
@@ -619,6 +619,137 @@ EXPOSE 80 443
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
```
+## Development Philosophy
+
+### Ship, Validate, Refine
+
+```ruby
+# 1. Merge prototype-quality code to test real usage
+# 2. Iterate based on real feedback
+# 3. Polish what works, remove what doesn't
+```
+
+DHH merges features early to validate in production. Perfect code that no one uses is worse than rough code that gets feedback.
+
+### Fix Root Causes
+
+```ruby
+# ✅ Prevent race conditions at the source
+config.active_job.enqueue_after_transaction_commit = true
+
+# ❌ Bandaid fix with retries
+retry_on ActiveRecord::RecordNotFound, wait: 1.second
+```
+
+Address underlying issues rather than symptoms.
+
+### Vanilla Rails Over Abstractions
+
+```ruby
+# ✅ Direct ActiveRecord
+@card.comments.create!(comment_params)
+
+# ❌ Service layer indirection
+CreateCommentService.call(@card, comment_params)
+```
+
+Use Rails conventions. Only abstract when genuine pain emerges.
+
+## Rails 7.1+ Idioms
+
+### params.expect (PR #120)
+
+```ruby
+# ✅ Rails 7.1+ style
+def card_params
+ params.expect(card: [:title, :description, tags: []])
+end
+
+# Returns 400 Bad Request if structure invalid
+
+# Old style
+def card_params
+ params.require(:card).permit(:title, :description, tags: [])
+end
+```
+
+### StringInquirer (PR #425)
+
+```ruby
+# ✅ Readable predicates
+event.action.inquiry.completed?
+event.action.inquiry.pending?
+
+# Usage
+case
+when event.action.inquiry.completed?
+ send_notification
+when event.action.inquiry.failed?
+ send_alert
+end
+
+# Old style
+event.action == "completed"
+```
+
+### Positive Naming
+
+```ruby
+# ✅ Positive names
+scope :active, -> { where(active: true) }
+scope :visible, -> { where(visible: true) }
+scope :published, -> { where.not(published_at: nil) }
+
+# ❌ Negative names
+scope :not_deleted, -> { ... } # Use :active
+scope :non_hidden, -> { ... } # Use :visible
+scope :is_not_draft, -> { ... } # Use :published
+```
+
+## Extraction Guidelines
+
+### Rule of Three
+
+```ruby
+# First time: Just do it inline
+def process
+ # inline logic
+end
+
+# Second time: Still inline, note the duplication
+def process_again
+ # same logic
+end
+
+# Third time: NOW extract
+module Processing
+ def shared_logic
+ # extracted
+ end
+end
+```
+
+Wait for genuine pain before extracting.
+
+### Start in Controller, Extract When Complex
+
+```ruby
+# Phase 1: Logic in controller
+def index
+ @cards = @board.cards.where(status: params[:status])
+end
+
+# Phase 2: Move to model scope
+def index
+ @cards = @board.cards.by_status(params[:status])
+end
+
+# Phase 3: Extract concern if reused
+def index
+ @cards = @board.cards.filtered(params)
+end
+```
+
## Anti-Patterns to Avoid
### Don't Add Service Objects for Simple Cases