From 141bbb42cb4f1c6c31349932f1ba556535a40885 Mon Sep 17 00:00:00 2001 From: Kieran Klaassen Date: Mon, 5 Jan 2026 09:36:56 -0800 Subject: [PATCH] [2.16.0] Consolidate DHH styles and add /feature-video command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge dhh-ruby-style into dhh-rails-style for comprehensive Rails conventions - Add testing.md reference covering Rails testing patterns - Add /feature-video command for recording PR demo videos - Update docs and component counts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude-plugin/marketplace.json | 4 +- docs/index.html | 30 +- docs/pages/skills.html | 67 +- .../.claude-plugin/plugin.json | 2 +- plugins/compound-engineering/CHANGELOG.md | 19 +- plugins/compound-engineering/README.md | 5 +- .../commands/feature-video.md | 342 ++++++++ .../skills/dhh-rails-style/SKILL.md | 102 ++- .../dhh-rails-style/references/testing.md | 338 +++++++ .../skills/dhh-ruby-style/SKILL.md | 201 ----- .../dhh-ruby-style/references/patterns.md | 830 ------------------ .../dhh-ruby-style/references/resources.md | 179 ---- 12 files changed, 845 insertions(+), 1274 deletions(-) create mode 100644 plugins/compound-engineering/commands/feature-video.md create mode 100644 plugins/compound-engineering/skills/dhh-rails-style/references/testing.md delete mode 100644 plugins/compound-engineering/skills/dhh-ruby-style/SKILL.md delete mode 100644 plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md delete mode 100644 plugins/compound-engineering/skills/dhh-ruby-style/references/resources.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b69e512..4ac1611 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,8 +11,8 @@ "plugins": [ { "name": "compound-engineering", - "description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 27 specialized agents, 19 commands, and 13 skills.", - "version": "2.18.0", + "description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 27 specialized agents, 20 commands, and 12 skills.", + "version": "2.20.0", "author": { "name": "Kieran Klaassen", "url": "https://github.com/kieranklaassen", diff --git a/docs/index.html b/docs/index.html index 191cd37..614bc5f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Compounding Engineering - AI-Powered Development Tools for Claude Code - + @@ -12,7 +12,7 @@ - + @@ -161,7 +161,7 @@ Your Code Reviews Just Got 12 Expert Opinions. In 30 Seconds.

- Here's what happened when we shipped yesterday: security audit, performance analysis, architectural review, pattern detection, and eight more specialized checks—all running in parallel. No meetings. No waiting. Just answers. That's compounding engineering: 24 specialized agents, 19 workflow commands, and 11 skills that make today's work easier than yesterday's. + Here's what happened when we shipped yesterday: security audit, performance analysis, architectural review, pattern detection, and eight more specialized checks—all running in parallel. No meetings. No waiting. Just answers. That's compounding engineering: 27 specialized agents, 19 workflow commands, and 12 skills that make today's work easier than yesterday's.

@@ -179,7 +179,7 @@
-
24
+
27
Specialized Agents
@@ -189,7 +189,7 @@
-
11
+
12
Intelligent Skills
@@ -244,9 +244,9 @@ The security-sentinel has checked 10,000 PRs for SQL injection. The kieran-rails-reviewer never approves a controller with business logic. They don't get tired, don't skip Friday afternoon reviews, don't forget the conventions you agreed on in March. Run /work and watch your plan execute with quality gates that actually enforce your standards—every single time.

- 24 specialized agents + 27 specialized agents /work - dhh-ruby-style skill + dhh-rails-style skill git-worktree skill
@@ -292,7 +292,7 @@

- 24 Specialized Agents + 27 Specialized Agents

Think of them as coworkers who never quit. The security-sentinel has seen every SQL injection variant. The kieran-rails-reviewer enforces conventions with zero compromise. The performance-oracle spots N+1 queries while you're still reading the PR. Run them solo or launch twelve in parallel—your choice. @@ -683,10 +683,10 @@

- 11 Intelligent Skills + 12 Intelligent Skills

- Domain expertise on tap. Need to write a Ruby gem? The andrew-kane-gem-writer knows the patterns Andrew uses in 50+ popular gems. Building a Rails app? The dhh-ruby-style enforces 37signals conventions. Generating images? The gemini-imagegen has Google's AI on speed dial. Just invoke the skill and watch it work. + Domain expertise on tap. Need to write a Ruby gem? The andrew-kane-gem-writer knows the patterns Andrew uses in 50+ popular gems. Building a Rails app? The dhh-rails-style enforces 37signals conventions. Generating images? The gemini-imagegen has Google's AI on speed dial. Just invoke the skill and watch it work.

@@ -704,11 +704,11 @@
- dhh-ruby-style + dhh-rails-style Rails
-

Write Ruby/Rails code in DHH's 37signals style. Convention over configuration, beautiful code.

- skill: dhh-ruby-style +

Write Ruby/Rails code in DHH's 37signals style. REST purity, fat models, thin controllers, Hotwire patterns.

+ skill: dhh-rails-style
@@ -943,7 +943,7 @@ skill: gemini-imagegen Agents are coworkers with specific jobs. The security-sentinel does security reviews. The kieran-rails-reviewer enforces Rails conventions. You call them directly: claude agent security-sentinel.

- Skills are expertise Claude can tap into when needed. The dhh-ruby-style knows 37signals patterns. The gemini-imagegen knows how to generate images. Claude invokes them automatically when relevant, or you can explicitly call them: skill: dhh-ruby-style. + Skills are expertise Claude can tap into when needed. The dhh-rails-style knows 37signals Rails patterns. The gemini-imagegen knows how to generate images. Claude invokes them automatically when relevant, or you can explicitly call them: skill: dhh-rails-style.

@@ -989,7 +989,7 @@ skill: gemini-imagegen Free & Open Source

Install Once. Compound Forever.

- Your next code review takes 30 seconds. The one after that? Even faster. That's compounding. Get 24 expert agents, 19 workflow commands, and 11 specialized skills working for you right now. + Your next code review takes 30 seconds. The one after that? Even faster. That's compounding. Get 27 expert agents, 19 workflow commands, and 12 specialized skills working for you right now.

@@ -101,7 +101,7 @@ skill: create-agent-skills
-

Development Tools (7)

+

Development Tools (8)

These skills teach Claude specific coding styles and architectural patterns. Use them when you want code that follows a particular philosophy—not just any working code, but code that looks like it was written by a specific person or framework.

@@ -158,31 +158,34 @@ skill: create-agent-skills
-
+
-

dhh-ruby-style

+

dhh-rails-style

Rails

- You want Rails controllers that are five lines, not fifty. Models that handle authorization, broadcasting, and business logic without service objects everywhere. This skill teaches Claude to write code the way DHH writes it at 37signals—REST-pure, Hotwire-first, no architectural astronautics. + Comprehensive 37signals Rails conventions based on Marc Köhlbrugge's analysis of 265 PRs from the Fizzy codebase. Covers everything from REST mapping to state-as-records, Turbo/Stimulus patterns, CSS with OKLCH colors, Minitest with fixtures, and Solid Queue/Cache/Cable patterns.

Key Patterns

    -
  • REST Purity - 7 REST actions only
  • -
  • Fat Models - Business logic, authorization, broadcasting in models
  • -
  • Thin Controllers - 1-5 line actions
  • -
  • Current Attributes - Request context
  • -
  • Hotwire/Turbo - Model-level broadcasting
  • +
  • REST Purity - Verbs become nouns (close → closure)
  • +
  • State as Records - Boolean columns → separate records
  • +
  • Fat Models - Business logic, authorization, broadcasting
  • +
  • Thin Controllers - 1-5 line actions with concerns
  • +
  • Current Attributes - Request context everywhere
  • +
  • Hotwire/Turbo - Model-level broadcasting, morphing
-

Ruby Syntax Preferences

+

Reference Files (6)

    -
  • Symbol arrays %i[...]
  • -
  • Modern hash syntax
  • -
  • Ternaries for simple conditionals
  • -
  • Bang methods for mutations
  • +
  • controllers.md - REST mapping, concerns, Turbo responses
  • +
  • models.md - Concerns, state records, callbacks, POROs
  • +
  • frontend.md - Turbo, Stimulus, CSS layers, OKLCH
  • +
  • architecture.md - Routing, auth, jobs, caching
  • +
  • testing.md - Minitest, fixtures, integration tests
  • +
  • gems.md - What to use vs avoid, decision framework
-
skill: dhh-ruby-style
+
skill: dhh-rails-style
@@ -331,6 +334,28 @@ skill: create-agent-skills
skill: compound-docs
+ +
+
+

agent-native-architecture

+ AI +
+

+ Build AI agents using prompt-native architecture where features are defined in prompts, not code. When creating autonomous agents, designing MCP servers, or implementing self-modifying systems, this skill guides the "trust the agent's intelligence" philosophy. +

+

Key Patterns

+
    +
  • Prompt-Native Features - Define features in prompts, not code
  • +
  • MCP Tool Design - Build tools agents can use effectively
  • +
  • System Prompts - Write instructions that guide agent behavior
  • +
  • Self-Modification - Allow agents to improve their own prompts
  • +
+

Core Principle

+

Whatever the user can do, the agent can do. Whatever the user can see, the agent can see.

+
+
skill: agent-native-architecture
+
+
diff --git a/plugins/compound-engineering/.claude-plugin/plugin.json b/plugins/compound-engineering/.claude-plugin/plugin.json index 4364758..85385bd 100644 --- a/plugins/compound-engineering/.claude-plugin/plugin.json +++ b/plugins/compound-engineering/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "compound-engineering", "version": "2.20.0", - "description": "AI-powered development tools. 27 agents, 20 commands, 13 skills, 2 MCP servers for code review, research, design, and workflow automation.", + "description": "AI-powered development tools. 27 agents, 20 commands, 12 skills, 2 MCP servers for code review, research, design, and workflow automation.", "author": { "name": "Kieran Klaassen", "email": "kieran@every.to", diff --git a/plugins/compound-engineering/CHANGELOG.md b/plugins/compound-engineering/CHANGELOG.md index 2e8b283..7b97381 100644 --- a/plugins/compound-engineering/CHANGELOG.md +++ b/plugins/compound-engineering/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to the compound-engineering plugin will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.20.0] - 2026-01-01 +## [2.20.0] - 2026-01-05 + +### Added + +- **`/feature-video` command** - Record video walkthroughs of features using Playwright. Captures screenshots during browser interactions, generates GIF/MP4 videos, supports upload to transfer.sh or manual upload, and automatically updates PR descriptions with embedded demos. Perfect for documenting UI changes in pull requests. ### Changed @@ -17,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `references/best-practices.md` - Skill authoring best practices from platform.claude.com - Removed obsolete `references/use-xml-tags.md` - this was incorrect guidance +### Removed + +- **`dhh-ruby-style` skill** - Merged into `dhh-rails-style` skill. All content consolidated into the single comprehensive Rails style guide. + ### Philosophy This update aligns the skill with Anthropic's official documentation. The key insight: **Skills are prompts**. All standard prompting best practices apply. Use standard markdown, not custom XML tags. Keep SKILL.md under 500 lines with progressive disclosure to reference files. @@ -103,11 +111,6 @@ These updates operationalize a key insight from building agent-native mobile app - **architecture.md** - Added path-based multi-tenancy, database patterns (UUIDs, state as records, hard deletes, counter caches), background job patterns (transaction safety, error handling, batch processing), email patterns, security patterns (XSS, SSRF, CSP), Active Storage patterns - **gems.md** - Added expanded what-they-avoid section (service objects, form objects, decorators, CSS preprocessors, React/Vue), testing philosophy with Minitest/fixtures patterns -- **`dhh-ruby-style` skill** - Expanded patterns.md with: - - Development philosophy (ship/validate/refine, fix root causes, vanilla Rails first) - - Rails 7.1+ idioms (params.expect, StringInquirer, positive naming conventions) - - Extraction guidelines (rule of three, start in controller extract when complex) - ### Credits - Reference patterns derived from [Marc Köhlbrugge's Unofficial 37signals Coding Style Guide](https://github.com/marckohlbrugge/unofficial-37signals-coding-style-guide) @@ -116,10 +119,10 @@ These updates operationalize a key insight from building agent-native mobile app ### Fixed -- **All skills** - Fixed spec compliance issues across 13 skills: +- **All skills** - Fixed spec compliance issues across 12 skills: - Reference files now use proper markdown links (`[file.md](./references/file.md)`) instead of backtick text - Descriptions now use third person ("This skill should be used when...") per skill-creator spec - - Affected skills: agent-native-architecture, andrew-kane-gem-writer, compound-docs, create-agent-skills, dhh-rails-style, dhh-ruby-style, dspy-ruby, every-style-editor, file-todos, frontend-design, gemini-imagegen + - Affected skills: agent-native-architecture, andrew-kane-gem-writer, compound-docs, create-agent-skills, dhh-rails-style, dspy-ruby, every-style-editor, file-todos, frontend-design, gemini-imagegen ### Added diff --git a/plugins/compound-engineering/README.md b/plugins/compound-engineering/README.md index f05f013..6c3ed36 100644 --- a/plugins/compound-engineering/README.md +++ b/plugins/compound-engineering/README.md @@ -8,7 +8,7 @@ AI-powered development tools that get smarter with every use. Make each unit of |-----------|-------| | Agents | 27 | | Commands | 20 | -| Skills | 13 | +| Skills | 12 | | MCP Servers | 2 | ## Agents @@ -98,6 +98,7 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in | `/triage` | Triage and prioritize issues | | `/playwright-test` | Run browser tests on PR-affected pages | | `/xcode-test` | Build and test iOS apps on simulator | +| `/feature-video` | Record video walkthroughs and add to PR description | ## Skills @@ -114,7 +115,7 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in | `andrew-kane-gem-writer` | Write Ruby gems following Andrew Kane's patterns | | `compound-docs` | Capture solved problems as categorized documentation | | `create-agent-skills` | Expert guidance for creating Claude Code skills | -| `dhh-ruby-style` | Write Ruby/Rails code in DHH's 37signals style | +| `dhh-rails-style` | Write Ruby/Rails code in DHH's 37signals style | | `dspy-ruby` | Build type-safe LLM applications with DSPy.rb | | `frontend-design` | Create production-grade frontend interfaces | | `skill-creator` | Guide for creating effective Claude Code skills | diff --git a/plugins/compound-engineering/commands/feature-video.md b/plugins/compound-engineering/commands/feature-video.md new file mode 100644 index 0000000..c1dfcf7 --- /dev/null +++ b/plugins/compound-engineering/commands/feature-video.md @@ -0,0 +1,342 @@ +--- +name: feature-video +description: Record a video walkthrough of a feature and add it to the PR description +argument-hint: "[PR number or 'current'] [optional: base URL, default localhost:3000]" +--- + +# Feature Video Walkthrough + +Record a video walkthrough demonstrating a feature, upload it, and add it to the PR description. + +## Introduction + +Developer Relations Engineer creating feature demo videos + +This command creates professional video walkthroughs of features for PR documentation: +- Records browser interactions using Playwright video capture +- Demonstrates the complete user flow +- Uploads the video for easy sharing +- Updates the PR description with an embedded video + +## Prerequisites + + +- Local development server running (e.g., `bin/dev`, `rails server`) +- Playwright MCP server connected +- Git repository with a PR to document +- `ffmpeg` installed (for video conversion if needed) + + +## Main Tasks + +### 1. Parse Arguments + + + +**Arguments:** $ARGUMENTS + +Parse the input: +- First argument: PR number or "current" (defaults to current branch's PR) +- Second argument: Base URL (defaults to `http://localhost:3000`) + +```bash +# Get PR number for current branch if needed +gh pr view --json number -q '.number' +``` + + + +### 2. Gather Feature Context + + + +**Get PR details:** +```bash +gh pr view [number] --json title,body,files,headRefName -q '.' +``` + +**Get changed files:** +```bash +gh pr view [number] --json files -q '.files[].path' +``` + +**Map files to testable routes** (same as playwright-test): + +| File Pattern | Route(s) | +|-------------|----------| +| `app/views/users/*` | `/users`, `/users/:id`, `/users/new` | +| `app/controllers/settings_controller.rb` | `/settings` | +| `app/javascript/controllers/*_controller.js` | Pages using that Stimulus controller | +| `app/components/*_component.rb` | Pages rendering that component | + + + +### 3. Plan the Video Flow + + + +Before recording, create a shot list: + +1. **Opening shot**: Homepage or starting point (2-3 seconds) +2. **Navigation**: How user gets to the feature +3. **Feature demonstration**: Core functionality (main focus) +4. **Edge cases**: Error states, validation, etc. (if applicable) +5. **Success state**: Completed action/result + +Ask user to confirm or adjust the flow: + +```markdown +**Proposed Video Flow** + +Based on PR #[number]: [title] + +1. Start at: /[starting-route] +2. Navigate to: /[feature-route] +3. Demonstrate: + - [Action 1] + - [Action 2] + - [Action 3] +4. Show result: [success state] + +Estimated duration: ~[X] seconds + +Does this look right? +1. Yes, start recording +2. Modify the flow (describe changes) +3. Add specific interactions to demonstrate +``` + + + +### 4. Setup Video Recording + + + +**Create videos directory:** +```bash +mkdir -p tmp/videos +``` + +**Start browser with video recording using Playwright MCP:** + +Note: Playwright MCP's browser_navigate will be used, and we'll use browser_run_code to enable video recording: + +```javascript +// Enable video recording context +mcp__plugin_compound-engineering_pw__browser_run_code({ + code: `async (page) => { + // Video recording is enabled at context level + // The MCP server handles this automatically + return 'Video recording active'; + }` +}) +``` + +**Alternative: Use browser screenshots as frames** + +If video recording isn't available via MCP, fall back to: +1. Take screenshots at key moments +2. Combine into a GIF using ffmpeg + +```bash +ffmpeg -framerate 2 -pattern_type glob -i 'tmp/screenshots/*.png' -vf "scale=1280:-1" tmp/videos/feature-demo.gif +``` + + + +### 5. Record the Walkthrough + + + +Execute the planned flow, capturing each step: + +**Step 1: Navigate to starting point** +``` +mcp__plugin_compound-engineering_pw__browser_navigate({ url: "[base-url]/[start-route]" }) +mcp__plugin_compound-engineering_pw__browser_wait_for({ time: 2 }) +mcp__plugin_compound-engineering_pw__browser_take_screenshot({ filename: "tmp/screenshots/01-start.png" }) +``` + +**Step 2: Perform navigation/interactions** +``` +mcp__plugin_compound-engineering_pw__browser_click({ element: "[description]", ref: "[ref]" }) +mcp__plugin_compound-engineering_pw__browser_wait_for({ time: 1 }) +mcp__plugin_compound-engineering_pw__browser_take_screenshot({ filename: "tmp/screenshots/02-navigate.png" }) +``` + +**Step 3: Demonstrate feature** +``` +mcp__plugin_compound-engineering_pw__browser_snapshot({}) +// Identify interactive elements +mcp__plugin_compound-engineering_pw__browser_click({ element: "[feature element]", ref: "[ref]" }) +mcp__plugin_compound-engineering_pw__browser_wait_for({ time: 1 }) +mcp__plugin_compound-engineering_pw__browser_take_screenshot({ filename: "tmp/screenshots/03-feature.png" }) +``` + +**Step 4: Capture result** +``` +mcp__plugin_compound-engineering_pw__browser_wait_for({ time: 2 }) +mcp__plugin_compound-engineering_pw__browser_take_screenshot({ filename: "tmp/screenshots/04-result.png" }) +``` + +**Create video/GIF from screenshots:** +```bash +# Create GIF from screenshots +ffmpeg -framerate 1 -pattern_type glob -i 'tmp/screenshots/*.png' \ + -vf "scale=1280:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \ + -loop 0 tmp/videos/feature-demo.gif + +# Or create MP4 for better quality +ffmpeg -framerate 1 -pattern_type glob -i 'tmp/screenshots/*.png' \ + -c:v libx264 -pix_fmt yuv420p -vf "scale=1280:-1" \ + tmp/videos/feature-demo.mp4 +``` + + + +### 6. Upload the Video + + + +**Option A: Upload to GitHub (via PR comment with attachment)** + +GitHub doesn't support direct video uploads via API, but you can: +1. Drag-drop in browser, or +2. Use a hosting service + +**Option B: Upload to transfer.sh (temporary, 14 days)** +```bash +curl --upload-file tmp/videos/feature-demo.gif https://transfer.sh/feature-demo.gif +``` + +**Option C: Upload to Cloudflare R2/S3 (if configured)** +```bash +# If AWS CLI is configured +aws s3 cp tmp/videos/feature-demo.gif s3://your-bucket/pr-videos/pr-[number]-demo.gif --acl public-read +``` + +**Option D: Keep local and provide path** +```bash +# Just provide the local path for manual upload +echo "Video saved to: $(pwd)/tmp/videos/feature-demo.gif" +``` + +Ask user for upload preference: +```markdown +**Video Ready** + +Video saved to: `tmp/videos/feature-demo.gif` +Size: [size] + +How would you like to share it? +1. Upload to transfer.sh (temporary link, 14 days) +2. Keep local - I'll upload manually +3. Upload to S3/R2 (requires config) +``` + + + +### 7. Update PR Description + + + +**Get current PR body:** +```bash +gh pr view [number] --json body -q '.body' +``` + +**Add video section to PR description:** + +If the PR already has a video section, replace it. Otherwise, append: + +```markdown +## Demo Video + +![Feature Demo]([video-url]) + +_Walkthrough recorded with Playwright_ +``` + +**Update the PR:** +```bash +gh pr edit [number] --body "[updated body with video section]" +``` + +**Or add as a comment if preferred:** +```bash +gh pr comment [number] --body "## Feature Demo + +![Demo]([video-url]) + +_Automated walkthrough of the changes in this PR_" +``` + + + +### 8. Cleanup + + + +```bash +# Optional: Clean up screenshots +rm -rf tmp/screenshots + +# Keep videos for reference +echo "Video retained at: tmp/videos/feature-demo.gif" +``` + + + +### 9. Summary + + + +Present completion summary: + +```markdown +## Feature Video Complete + +**PR:** #[number] - [title] +**Video:** [url or local path] +**Duration:** ~[X] seconds +**Format:** [GIF/MP4] + +### Shots Captured +1. [Starting point] - [description] +2. [Navigation] - [description] +3. [Feature demo] - [description] +4. [Result] - [description] + +### PR Updated +- [x] Video section added to PR description +- [ ] Ready for review + +**Next steps:** +- Review the video to ensure it accurately demonstrates the feature +- Share with reviewers for context +``` + + + +## Quick Usage Examples + +```bash +# Record video for current branch's PR +/feature-video + +# Record video for specific PR +/feature-video 847 + +# Record with custom base URL +/feature-video 847 http://localhost:5000 + +# Record for staging environment +/feature-video current https://staging.example.com +``` + +## Tips + +- **Keep it short**: 10-30 seconds is ideal for PR demos +- **Focus on the change**: Don't include unrelated UI +- **Show before/after**: If fixing a bug, show the broken state first (if possible) +- **Annotate if needed**: Add text overlays for complex features diff --git a/plugins/compound-engineering/skills/dhh-rails-style/SKILL.md b/plugins/compound-engineering/skills/dhh-rails-style/SKILL.md index 4e1fc24..d922e82 100644 --- a/plugins/compound-engineering/skills/dhh-rails-style/SKILL.md +++ b/plugins/compound-engineering/skills/dhh-rails-style/SKILL.md @@ -4,7 +4,7 @@ description: This skill should be used when writing Ruby and Rails code in DHH's --- -Apply 37signals/DHH Rails conventions to Ruby and Rails code. This skill provides domain expertise extracted from analyzing production 37signals codebases (Fizzy/Campfire). +Apply 37signals/DHH Rails conventions to Ruby and Rails code. This skill provides comprehensive domain expertise extracted from analyzing production 37signals codebases (Fizzy/Campfire) and DHH's code review patterns. @@ -27,17 +27,28 @@ Apply 37signals/DHH Rails conventions to Ruby and Rails code. This skill provide - redis (database for everything) - view_component (partials work fine) - GraphQL (REST with Turbo sufficient) +- factory_bot (fixtures are simpler) +- rspec (Minitest ships with Rails) +- Tailwind (native CSS with layers) + +**Development Philosophy:** +- Ship, Validate, Refine - prototype-quality code to production to learn +- Fix root causes, not symptoms +- Write-time operations over read-time computations +- Database constraints over ActiveRecord validations What are you working on? -1. **Controllers** - REST mapping, concerns, Turbo responses -2. **Models** - Concerns, state records, callbacks, scopes +1. **Controllers** - REST mapping, concerns, Turbo responses, API patterns +2. **Models** - Concerns, state records, callbacks, scopes, POROs 3. **Views & Frontend** - Turbo, Stimulus, CSS, partials -4. **Architecture** - Routing, multi-tenancy, authentication, jobs -5. **Code Review** - Review code against DHH style -6. **General Guidance** - Philosophy and conventions +4. **Architecture** - Routing, multi-tenancy, authentication, jobs, caching +5. **Testing** - Minitest, fixtures, integration tests +6. **Gems & Dependencies** - What to use vs avoid +7. **Code Review** - Review code against DHH style +8. **General Guidance** - Philosophy and conventions **Specify a number or describe your task.** @@ -48,9 +59,11 @@ What are you working on? | 1, "controller" | [controllers.md](./references/controllers.md) | | 2, "model" | [models.md](./references/models.md) | | 3, "view", "frontend", "turbo", "stimulus", "css" | [frontend.md](./references/frontend.md) | -| 4, "architecture", "routing", "auth", "job" | [architecture.md](./references/architecture.md) | -| 5, "review" | Read all references, then review code | -| 6, general task | Read relevant references based on context | +| 4, "architecture", "routing", "auth", "job", "cache" | [architecture.md](./references/architecture.md) | +| 5, "test", "testing", "minitest", "fixture" | [testing.md](./references/testing.md) | +| 6, "gem", "dependency", "library" | [gems.md](./references/gems.md) | +| 7, "review" | Read all references, then review code | +| 8, general task | Read relevant references based on context | **After reading relevant references, apply patterns to the user's code.** @@ -70,6 +83,7 @@ What are you working on? - `chronologically`, `reverse_chronologically`, `alphabetically`, `latest` - `preloaded` (standard eager loading name) - `indexed_by`, `sorted_by` (parameterized) +- `active`, `unassigned` (business terms, not SQL-ish) ## REST Mapping @@ -80,6 +94,55 @@ POST /cards/:id/close → POST /cards/:id/closure DELETE /cards/:id/close → DELETE /cards/:id/closure POST /cards/:id/archive → POST /cards/:id/archival ``` + +## Ruby Syntax Preferences + +```ruby +# Symbol arrays with spaces inside brackets +before_action :set_message, only: %i[ show edit update destroy ] + +# Private method indentation + private + def set_message + @message = Message.find(params[:id]) + end + +# Expression-less case for conditionals +case +when params[:before].present? + messages.page_before(params[:before]) +else + messages.last_page +end + +# Bang methods for fail-fast +@message = Message.create!(params) + +# Ternaries for simple conditionals +@room.direct? ? @room.users : @message.mentionees +``` + +## Key Patterns + +**State as Records:** +```ruby +Card.joins(:closure) # closed cards +Card.where.missing(:closure) # open cards +``` + +**Current Attributes:** +```ruby +belongs_to :creator, default: -> { Current.user } +``` + +**Authorization on Models:** +```ruby +class User < ApplicationRecord + def can_administer?(message) + message.creator == self || admin? + end +end +``` @@ -89,11 +152,12 @@ All detailed patterns in `references/`: | File | Topics | |------|--------| -| [controllers.md](./references/controllers.md) | REST mapping, concerns, Turbo responses, API patterns | -| [models.md](./references/models.md) | Concerns, state records, callbacks, scopes, POROs | -| [frontend.md](./references/frontend.md) | Turbo, Stimulus, CSS architecture, view patterns | -| [architecture.md](./references/architecture.md) | Routing, auth, jobs, caching, multi-tenancy, config | -| [gems.md](./references/gems.md) | What they use vs avoid, and why | +| [controllers.md](./references/controllers.md) | REST mapping, concerns, Turbo responses, API patterns, HTTP caching | +| [models.md](./references/models.md) | Concerns, state records, callbacks, scopes, POROs, authorization, broadcasting | +| [frontend.md](./references/frontend.md) | Turbo Streams, Stimulus controllers, CSS layers, OKLCH colors, partials | +| [architecture.md](./references/architecture.md) | Routing, authentication, jobs, Current attributes, caching, database patterns | +| [testing.md](./references/testing.md) | Minitest, fixtures, unit/integration/system tests, testing patterns | +| [gems.md](./references/gems.md) | What they use vs avoid, decision framework, Gemfile examples | @@ -105,8 +169,16 @@ Code follows DHH style when: - Database-backed solutions preferred over external services - Tests use Minitest with fixtures - Turbo/Stimulus for interactivity (no heavy JS frameworks) +- Native CSS with modern features (layers, OKLCH, nesting) +- Authorization logic lives on User model +- Jobs are shallow wrappers calling model methods -Based on [The Unofficial 37signals/DHH Rails Style Guide](https://gist.github.com/marckohlbrugge/d363fb90c89f71bd0c816d24d7642aca) by [Marc Köhlbrugge](https://x.com/marckohlbrugge), generated through deep analysis of the Fizzy codebase. +Based on [The Unofficial 37signals/DHH Rails Style Guide](https://github.com/marckohlbrugge/unofficial-37signals-coding-style-guide) by [Marc Köhlbrugge](https://x.com/marckohlbrugge), generated through deep analysis of 265 pull requests from the Fizzy codebase. + +**Important Disclaimers:** +- LLM-generated guide - may contain inaccuracies +- Code examples from Fizzy are licensed under the O'Saasy License +- Not affiliated with or endorsed by 37signals diff --git a/plugins/compound-engineering/skills/dhh-rails-style/references/testing.md b/plugins/compound-engineering/skills/dhh-rails-style/references/testing.md new file mode 100644 index 0000000..4316fad --- /dev/null +++ b/plugins/compound-engineering/skills/dhh-rails-style/references/testing.md @@ -0,0 +1,338 @@ +# Testing - DHH Rails Style + +## Core Philosophy + +"Minitest with fixtures - simple, fast, deterministic." The approach prioritizes pragmatism over convention. + +## Why Minitest Over RSpec + +- **Simpler**: Less DSL magic, plain Ruby assertions +- **Ships with Rails**: No additional dependencies +- **Faster boot times**: Less overhead +- **Plain Ruby**: No specialized syntax to learn + +## Fixtures as Test Data + +Rather than factories, fixtures provide preloaded data: +- Loaded once, reused across tests +- No runtime object creation overhead +- Explicit relationship visibility +- Deterministic IDs for easier debugging + +### Fixture Structure +```yaml +# test/fixtures/users.yml +david: + identity: david + account: basecamp + role: admin + +jason: + identity: jason + account: basecamp + role: member + +# test/fixtures/rooms.yml +watercooler: + name: Water Cooler + creator: david + direct: false + +# test/fixtures/messages.yml +greeting: + body: Hello everyone! + room: watercooler + creator: david +``` + +### Using Fixtures in Tests +```ruby +test "sending a message" do + user = users(:david) + room = rooms(:watercooler) + + # Test with fixture data +end +``` + +### Dynamic Fixture Values +ERB enables time-sensitive data: +```yaml +recent_card: + title: Recent Card + created_at: <%= 1.hour.ago %> + +old_card: + title: Old Card + created_at: <%= 1.month.ago %> +``` + +## Test Organization + +### Unit Tests +Verify business logic using setup blocks and standard assertions: + +```ruby +class CardTest < ActiveSupport::TestCase + setup do + @card = cards(:one) + @user = users(:david) + end + + test "closing a card creates a closure" do + assert_difference -> { Card::Closure.count } do + @card.close(creator: @user) + end + + assert @card.closed? + assert_equal @user, @card.closure.creator + end + + test "reopening a card destroys the closure" do + @card.close(creator: @user) + + assert_difference -> { Card::Closure.count }, -1 do + @card.reopen + end + + refute @card.closed? + end +end +``` + +### Integration Tests +Test full request/response cycles: + +```ruby +class CardsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:david) + sign_in @user + end + + test "closing a card" do + card = cards(:one) + + post card_closure_path(card) + + assert_response :success + assert card.reload.closed? + end + + test "unauthorized user cannot close card" do + sign_in users(:guest) + card = cards(:one) + + post card_closure_path(card) + + assert_response :forbidden + refute card.reload.closed? + end +end +``` + +### System Tests +Browser-based tests using Capybara: + +```ruby +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 + + test "drag and drop card to new column" do + sign_in users(:david) + visit board_path(boards(:main)) + + card = find("#card_#{cards(:one).id}") + target = find("#column_#{columns(:done).id}") + + card.drag_to target + + assert_selector "#column_#{columns(:done).id} #card_#{cards(:one).id}" + end +end +``` + +## Advanced Patterns + +### Time Testing +Use `travel_to` for deterministic time-dependent assertions: + +```ruby +test "card expires after 30 days" do + card = cards(:one) + + travel_to 31.days.from_now do + assert card.expired? + end +end +``` + +### External API Testing with VCR +Record and replay HTTP interactions: + +```ruby +test "fetches user data from API" do + VCR.use_cassette("user_api") do + user_data = ExternalApi.fetch_user(123) + + assert_equal "John", user_data[:name] + end +end +``` + +### Background Job Testing +Assert job enqueueing and email delivery: + +```ruby +test "closing card enqueues notification job" do + card = cards(:one) + + assert_enqueued_with(job: NotifyWatchersJob, args: [card]) do + card.close + end +end + +test "welcome email is sent on signup" do + assert_emails 1 do + Identity.create!(email: "new@example.com") + end +end +``` + +### Testing Turbo Streams +```ruby +test "message creation broadcasts to room" do + room = rooms(:watercooler) + + assert_turbo_stream_broadcasts [room, :messages] do + room.messages.create!(body: "Test", creator: users(:david)) + end +end +``` + +## Testing Principles + +### 1. Test Observable Behavior +Focus on what the code does, not how it does it: + +```ruby +# ❌ Testing implementation +test "calls notify method on each watcher" do + card.expects(:notify).times(3) + card.close +end + +# ✅ Testing behavior +test "watchers receive notifications when card closes" do + assert_difference -> { Notification.count }, 3 do + card.close + end +end +``` + +### 2. 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 +``` + +### 3. Tests Ship with Features +Same commit, not TDD-first but together. Neither before (strict TDD) nor after (deferred testing). + +### 4. Security Fixes Always Include Regression Tests +Every security fix must include a test that would have caught the vulnerability. + +### 5. Integration Tests Validate Complete Workflows +Don't just test individual pieces - test that they work together. + +## File Organization + +``` +test/ +├── controllers/ # Integration tests for controllers +├── fixtures/ # YAML fixtures for all models +├── helpers/ # Helper method tests +├── integration/ # API integration tests +├── jobs/ # Background job tests +├── mailers/ # Mailer tests +├── models/ # Unit tests for models +├── system/ # Browser-based system tests +└── test_helper.rb # Test configuration +``` + +## Test Helper Setup + +```ruby +# test/test_helper.rb +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +class ActiveSupport::TestCase + fixtures :all + + parallelize(workers: :number_of_processors) +end + +class ActionDispatch::IntegrationTest + include SignInHelper +end + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome +end +``` + +## Sign In Helper + +```ruby +# test/support/sign_in_helper.rb +module SignInHelper + def sign_in(user) + session = user.identity.sessions.create! + cookies.signed[:session_id] = session.id + end +end +``` diff --git a/plugins/compound-engineering/skills/dhh-ruby-style/SKILL.md b/plugins/compound-engineering/skills/dhh-ruby-style/SKILL.md deleted file mode 100644 index dc2a11f..0000000 --- a/plugins/compound-engineering/skills/dhh-ruby-style/SKILL.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -name: dhh-ruby-style -description: This skill should be used when writing Ruby and Rails code in DHH's distinctive 37signals style. It applies when writing Ruby code, Rails applications, creating models, controllers, or any Ruby file. Triggers on Ruby/Rails code generation, refactoring requests, code review, or when the user mentions DHH, 37signals, Basecamp, HEY, or Campfire style. Embodies REST purity, fat models, thin controllers, Current attributes, Hotwire patterns, and the "clarity over cleverness" philosophy. ---- - -# DHH Ruby/Rails Style Guide - -Write Ruby and Rails code following DHH's philosophy: **clarity over cleverness**, **convention over configuration**, **developer happiness** above all. - -## Quick Reference - -### Controller Actions -- **Only 7 REST actions**: `index`, `show`, `new`, `create`, `edit`, `update`, `destroy` -- **New behavior?** Create a new controller, not a custom action -- **Action length**: 1-5 lines maximum -- **Empty actions are fine**: Let Rails convention handle rendering - -```ruby -class MessagesController < ApplicationController - before_action :set_message, only: %i[ show edit update destroy ] - - def index - @messages = @room.messages.with_creator.last_page - fresh_when @messages - end - - def show - end - - def create - @message = @room.messages.create_with_attachment!(message_params) - @message.broadcast_create - end - - private - def set_message - @message = @room.messages.find(params[:id]) - end - - def message_params - params.require(:message).permit(:body, :attachment) - end -end -``` - -### Private Method Indentation -Indent private methods one level under `private` keyword: - -```ruby - private - def set_message - @message = Message.find(params[:id]) - end - - def message_params - params.require(:message).permit(:body) - end -``` - -### Model Design (Fat Models) -Models own business logic, authorization, and broadcasting: - -```ruby -class Message < ApplicationRecord - belongs_to :room - belongs_to :creator, class_name: "User" - has_many :mentions - - scope :with_creator, -> { includes(:creator) } - scope :page_before, ->(cursor) { where("id < ?", cursor.id).order(id: :desc).limit(50) } - - def broadcast_create - broadcast_append_to room, :messages, target: "messages" - end - - def mentionees - mentions.includes(:user).map(&:user) - end -end - -class User < ApplicationRecord - def can_administer?(message) - message.creator == self || admin? - end -end -``` - -### Current Attributes -Use `Current` for request context, never pass `current_user` everywhere: - -```ruby -class Current < ActiveSupport::CurrentAttributes - attribute :user, :session -end - -# Usage anywhere in app -Current.user.can_administer?(@message) -``` - -### Ruby Syntax Preferences - -```ruby -# Symbol arrays with spaces inside brackets -before_action :set_message, only: %i[ show edit update destroy ] - -# Modern hash syntax exclusively -params.require(:message).permit(:body, :attachment) - -# Single-line blocks with braces -users.each { |user| user.notify } - -# Ternaries for simple conditionals -@room.direct? ? @room.users : @message.mentionees - -# Bang methods for fail-fast -@message = Message.create!(params) -@message.update!(message_params) - -# Predicate methods with question marks -@room.direct? -user.can_administer?(@message) -@messages.any? - -# Expression-less case for cleaner conditionals -case -when params[:before].present? - @room.messages.page_before(params[:before]) -when params[:after].present? - @room.messages.page_after(params[:after]) -else - @room.messages.last_page -end -``` - -### Naming Conventions - -| Element | Convention | Example | -|---------|------------|---------| -| Setter methods | `set_` prefix | `set_message`, `set_room` | -| Parameter methods | `{model}_params` | `message_params` | -| Association names | Semantic, not generic | `creator` not `user` | -| Scopes | Chainable, descriptive | `with_creator`, `page_before` | -| Predicates | End with `?` | `direct?`, `can_administer?` | - -### Hotwire/Turbo Patterns -Broadcasting is model responsibility: - -```ruby -# In model -def broadcast_create - broadcast_append_to room, :messages, target: "messages" -end - -# In controller -@message.broadcast_replace_to @room, :messages, - target: [ @message, :presentation ], - partial: "messages/presentation", - attributes: { maintain_scroll: true } -``` - -### Error Handling -Rescue specific exceptions, fail fast with bang methods: - -```ruby -def create - @message = @room.messages.create_with_attachment!(message_params) - @message.broadcast_create -rescue ActiveRecord::RecordNotFound - render action: :room_not_found -end -``` - -### Architecture Preferences - -| Traditional | DHH Way | -|-------------|---------| -| PostgreSQL | SQLite (for single-tenant) | -| Redis + Sidekiq | Solid Queue | -| Redis cache | Solid Cache | -| Kubernetes | Single Docker container | -| Service objects | Fat models | -| Policy objects (Pundit) | Authorization on User model | -| FactoryBot | Fixtures | - -## Detailed References - -For comprehensive patterns and examples, see: -- [patterns.md](./references/patterns.md) - Complete code patterns with explanations -- [resources.md](./references/resources.md) - Links to source material and further reading - -## Philosophy Summary - -1. **REST purity**: 7 actions only; new controllers for variations -2. **Fat models**: Authorization, broadcasting, business logic in models -3. **Thin controllers**: 1-5 line actions; extract complexity -4. **Convention over configuration**: Empty methods, implicit rendering -5. **Minimal abstractions**: No service objects for simple cases -6. **Current attributes**: Thread-local request context everywhere -7. **Hotwire-first**: Model-level broadcasting, Turbo Streams, Stimulus -8. **Readable code**: Semantic naming, small methods, no comments needed -9. **Pragmatic testing**: System tests over unit tests, real integrations diff --git a/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md b/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md deleted file mode 100644 index be6b238..0000000 --- a/plugins/compound-engineering/skills/dhh-ruby-style/references/patterns.md +++ /dev/null @@ -1,830 +0,0 @@ -# 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 -
- <%= 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 %> -
-``` - -## 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"] -``` - -## 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 - -```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 -``` diff --git a/plugins/compound-engineering/skills/dhh-ruby-style/references/resources.md b/plugins/compound-engineering/skills/dhh-ruby-style/references/resources.md deleted file mode 100644 index 3ef5c0e..0000000 --- a/plugins/compound-engineering/skills/dhh-ruby-style/references/resources.md +++ /dev/null @@ -1,179 +0,0 @@ -# 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