Agent Skills - Rails Development

Table of Contents

1. Rails Development Skills

Skills for Ruby on Rails development, covering conventions, security, performance, and testing.

1.1. Overview

Rails has strong conventions and best practices. These skills help enforce Rails patterns, detect performance issues, identify security vulnerabilities, ensure migration safety, and maintain comprehensive test coverage.

These skills activate automatically when working in Rails projects (detected by presence of Gemfile, config/routes.rb, or app/ directory).

Note: Requires tool permissions in .claude/settings.json:

  • Bash(bundle:*)
  • Bash(rails:*)
  • Bash(bundle exec:*)
  • Bash(bundle audit:*)

1.2. Rails Convention Enforcer

Automatically enforce Rails conventions for file placement, naming, routing patterns, and architectural structure.

---
name: Rails Convention Enforcer
description: Automatically enforce Rails conventions for file placement, naming, routing, and architectural patterns when working in Ruby on Rails projects
allowed-tools:
  - Read
  - Grep
  - Bash(bundle:*)
  - Bash(rails:*)
---

# Rails Convention Enforcer

## Activation Triggers

Automatically activate when:
- Detecting Rails project (=Gemfile= with =gem 'rails'=, =config/application.rb=, =app/= directory)
- Creating new controllers, models, views, migrations
- Modifying =config/routes.rb=
- Adding services, concerns, helpers, or jobs
- File or class naming inconsistencies detected
- User mentions "Rails conventions" or "Rails structure"

## Convention Categories

### 1. File Placement & Directory Structure

Rails has a specific directory structure. Check that files are in the correct location:

**Standard Directories:**
- =app/models/= → ActiveRecord models, model concerns
- =app/controllers/= → Controller classes, controller concerns
- =app/views/= → View templates (ERB, Haml, Slim)
- =app/helpers/= → View helpers
- =app/services/= → Service objects (business logic)
- =app/jobs/= → Background jobs (ActiveJob, Sidekiq)
- =app/mailers/= → Email mailers
- =app/channels/= → Action Cable channels (WebSockets)
- =config/= → Configuration files, initializers, routes
- =db/migrate/= → Database migrations
- =lib/= → Non-app code, custom libraries
- =spec/= or =test/= → Tests (RSpec or Minitest)

**Common Mistakes:**
```ruby
# WRONG - service in models directory
app/models/user_registration_service.rb

# CORRECT - service in services directory
app/services/user_registration_service.rb
```

### 2. Naming Conventions

Rails uses strong naming conventions. Enforce consistent naming:

**Controllers:**
- File: Plural, snake_case, ends with =_controller.rb=
- Class: Plural, PascalCase, ends with =Controller=
- Example: =app/controllers/users_controller.rb= → =UsersController=

**Models:**
- File: Singular, snake_case, ends with =.rb=
- Class: Singular, PascalCase
- Example: =app/models/user.rb= → =User=

**Database Tables:**
- Plural, snake_case
- Example: =users=, =blog_posts=, =order_items=

**Associations:**
- =has_many= → plural, snake_case (=has_many :posts=)
- =belongs_to= → singular, snake_case (=belongs_to :user=)
- =has_one= → singular, snake_case (=has_one :profile=)

**Views:**
- Directory: Plural, snake_case, matches controller name
- Files: Action name, snake_case
- Example: =app/views/users/show.html.erb=

**Examples:**

**Good:**
```ruby
# app/controllers/blog_posts_controller.rb
class BlogPostsController < ApplicationController
  def index
    @blog_posts = BlogPost.all
  end
end

# app/models/blog_post.rb
class BlogPost < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments
end
```

**Bad:**
```ruby
# WRONG - singular controller name
# app/controllers/blog_post_controller.rb
class BlogPostController < ApplicationController
end

# WRONG - plural model name
# app/models/blog_posts.rb
class BlogPosts < ApplicationRecord
end
```

### 3. RESTful Routing Patterns

Rails promotes RESTful routes. Encourage standard resource routes:

**Standard REST Actions:**
- =index= - List resources (GET /users)
- =show= - Show one resource (GET /users/:id)
- =new= - Form to create (GET /users/new)
- =create= - Create resource (POST /users)
- =edit= - Form to edit (GET /users/:id/edit)
- =update= - Update resource (PATCH /users/:id)
- =destroy= - Delete resource (DELETE /users/:id)

**Good Routing:**
```ruby
# config/routes.rb
resources :users
resources :posts, only: [:index, :show]
resources :comments, except: [:destroy]

# Nested resources
resources :users do
  resources :posts
end
```

**Bad Routing:**
```ruby
# AVOID - non-RESTful custom routes
get '/get_user/:id', to: 'users#get_user'
post '/create_user', to: 'users#create_user'
get '/delete_user/:id', to: 'users#delete_user'

# Instead use:
resources :users, only: [:show, :create, :destroy]
```

**Custom Actions:**
When adding custom actions, use =member= or =collection=:
```ruby
resources :posts do
  member do
    post :publish      # /posts/:id/publish
    post :unpublish
  end

  collection do
    get :archived      # /posts/archived
  end
end
```

### 4. MVC Boundaries (Separation of Concerns)

Rails follows Model-View-Controller pattern. Enforce proper separation:

**Models:**
- Business logic, validations, associations
- Database queries (scopes, class methods)
- Callbacks (before_save, after_create, etc.)

**Controllers:**
- Handle HTTP requests/responses
- Authentication/authorization checks
- Set instance variables for views
- Render or redirect
- Should be thin (delegate to models/services)

**Views:**
- Display data
- Simple logic via helpers
- NO database queries
- NO business logic

**Good:**
```ruby
# app/models/user.rb
class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true

  scope :active, -> { where(active: true) }

  def full_name
    "#{first_name} #{last_name}"
  end
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.active.order(created_at: :desc)
  end
end

# app/views/users/index.html.erb
<% @users.each do |user| %>
  <p><%= user.full_name %></p>
<% end %>
```

**Bad:**
```ruby
# BAD - business logic in controller
class UsersController < ApplicationController
  def index
    @users = User.where("active = ? AND created_at > ?", true, 30.days.ago)
                 .order(created_at: :desc)
  end
end

# BAD - database query in view
# app/views/users/show.html.erb
<%= User.find(params[:id]).posts.count %>  # Query in view!
```

### 5. Rails Idioms

Encourage Rails-specific patterns and idioms:

**Use Concerns Over Deep Inheritance:**
```ruby
# Good - use concerns
# app/models/concerns/taggable.rb
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :tags, as: :taggable
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  include Taggable
end
```

**Scopes for Queries:**
```ruby
# Good - use scopes
class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
end

# Usage
Article.published.recent
```

**before_action for Filters:**
```ruby
# Good
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  private

  def set_post
    @post = Post.find(params[:id])
  end
end
```

**Service Objects for Complex Logic:**
```ruby
# Good - extract complex operations to services
# app/services/user_registration_service.rb
class UserRegistrationService
  def initialize(user_params)
    @user_params = user_params
  end

  def call
    user = User.new(@user_params)
    if user.save
      UserMailer.welcome_email(user).deliver_later
      CreateUserProfileJob.perform_later(user.id)
    end
    user
  end
end

# In controller
def create
  service = UserRegistrationService.new(user_params)
  @user = service.call

  if @user.persisted?
    redirect_to @user
  else
    render :new
  end
end
```

### 6. Strong Parameters Location

Always use strong parameters in controllers:

```ruby
# Good - strong parameters in private method
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    # ...
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :bio)
  end
end

# BAD - accepting all params
def create
  @user = User.new(params[:user])  # Security vulnerability!
end
```

## Detection Process

When reviewing Rails code:

1. **Check file location** - Verify file is in correct directory
2. **Verify naming** - Check file names match class names and conventions
3. **Scan routes** - Look for non-RESTful patterns
4. **Check layer boundaries** - Ensure logic is in correct layer (no queries in views)
5. **Review patterns** - Suggest concerns, scopes, services where appropriate
6. **Suggest corrections** - Provide specific guidance on how to fix violations

## Examples

### Example 1: Controller Naming

**User Code:**
```ruby
# app/controllers/user.rb
class User < ApplicationController
end
```

**Issue:** Controller has wrong name (should be plural and end with Controller)

**Suggestion:**
"The controller should be named =UsersController= (plural) and the file should be =users_controller.rb=. Rails convention is:
- File: =app/controllers/users_controller.rb=
- Class: =class UsersController < ApplicationController="

### Example 2: Non-RESTful Routes

**User Code:**
```ruby
# config/routes.rb
get '/show_user/:id', to: 'users#show_user'
post '/add_user', to: 'users#add_user'
```

**Issue:** Non-RESTful route naming

**Suggestion:**
"These routes don't follow REST conventions. Rails has built-in RESTful routing:

```ruby
resources :users, only: [:show, :create]
```

This creates:
- =GET /users/:id= → =users#show=
- =POST /users= → =users#create=
"

### Example 3: Business Logic in Controller

**User Code:**
```ruby
class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    @order.total = @order.line_items.sum(&:price)
    @order.tax = @order.total * 0.08
    @order.grand_total = @order.total + @order.tax

    if @order.save
      OrderMailer.confirmation(@order).deliver_later
      redirect_to @order
    else
      render :new
    end
  end
end
```

**Issue:** Business logic in controller

**Suggestion:**
"This controller has too much business logic. Move calculations to the model:

```ruby
# app/models/order.rb
class Order < ApplicationRecord
  before_save :calculate_totals
  after_create :send_confirmation

  private

  def calculate_totals
    self.total = line_items.sum(&:price)
    self.tax = total * 0.08
    self.grand_total = total + tax
  end

  def send_confirmation
    OrderMailer.confirmation(self).deliver_later
  end
end

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)

    if @order.save
      redirect_to @order
    else
      render :new
    end
  end
end
```

Or use a service object if the logic is more complex."

## Best Practices

### Keep Controllers Thin

Controllers should:
- Set up data for views
- Handle request/response
- Delegate business logic to models/services

Aim for 5-7 lines per action (excluding private methods).

### Fat Models or Service Objects

Business logic goes in:
- Models (for simple logic related to one model)
- Service objects (for complex operations involving multiple models)

### Use Concerns for Shared Behavior

Instead of inheritance, use concerns to share behavior across models or controllers.

### Follow REST When Possible

RESTful routes make your API predictable and easier to understand. Only deviate when there's a good reason.

### Consistent Naming

Consistent naming makes code navigable. Follow Rails conventions strictly for:
- Files
- Classes
- Methods
- Routes
- Database tables/columns

## Tools for Convention Checking

**RuboCop with Rails cops:**
```ruby
# .rubocop.yml
require:
  - rubocop-rails

Rails:
  Enabled: true
```

**Reek for code smells:**
Detects long methods, too many instance variables, etc.

**Rails Best Practices gem:**
Static code analyzer for Rails-specific anti-patterns.

## When to Skip

Some conventions can be broken with good reason:
- Legacy code migration (gradual refactoring)
- Third-party gem integration requirements
- Performance optimizations (with documentation)
- Explicit user/team decision (document in README)

Always explain why a convention is being violated if it's intentional.

1.3. N+1 Query Detector

Detect N+1 query problems in Rails code by identifying missing eager loading.

---
name: N+1 Query Detector
description: Detect N+1 query problems in Rails code by identifying missing includes, eager_load, or preload statements when iterating over associations
allowed-tools:
  - Read
  - Grep
  - Bash(bundle exec:*)
---

# N+1 Query Detector

## Activation Triggers

Automatically activate when:
- Viewing controller actions with ActiveRecord queries
- Examining code that iterates over model collections
- Association access patterns detected (=.posts=, =.comments=, etc.)
- User mentions "slow queries", "performance", or "database"
- Rails logs show repeated similar queries
- Testing or profiling code

## What is an N+1 Query?

An N+1 query happens when:
1. You load N records (e.g., 10 users)
2. For each record, you trigger another query (e.g., get that user's posts)
3. Result: 1 query + N queries = N+1 total queries

**Example:**
```ruby
# 1 query to get users
users = User.all  # SELECT * FROM users

# N queries (one per user)
users.each do |user|
  puts user.posts.count  # SELECT COUNT(*) FROM posts WHERE user_id = ?
end

# Total: 1 + N queries (if 100 users = 101 queries!)
```

## Detection Patterns

### Pattern 1: Association Iteration Without Eager Loading

**Problem:**
```ruby
# Controller
@users = User.all

# View
<% @users.each do |user| %>
  <%= user.posts.count %>  # N+1!
  <%= user.profile.avatar %>  # Another N+1!
<% end %>
```

**Solution:**
```ruby
# Controller - eager load associations
@users = User.includes(:posts, :profile)

# Now the view uses preloaded data, no extra queries
```

### Pattern 2: Mapping Over Collections

**Problem:**
```ruby
User.all.map do |user|
  {
    name: user.name,
    post_count: user.posts.count  # N+1
  }
end
```

**Solution:**
```ruby
User.includes(:posts).map do |user|
  {
    name: user.name,
    post_count: user.posts.size  # Uses preloaded association
  }
end

# Or better - use counter cache or database aggregation:
User.left_joins(:posts)
    .select('users.*, COUNT(posts.id) as post_count')
    .group('users.id')
```

### Pattern 3: Nested Associations

**Problem:**
```ruby
User.includes(:posts)  # Only loads posts

users.each do |user|
  user.posts.each do |post|
    post.comments.count  # N+1 on comments!
  end
end
```

**Solution:**
```ruby
# Use nested includes
User.includes(posts: :comments)

# For deeply nested:
User.includes(posts: { comments: :author })
```

### Pattern 4: Conditional Association Access

**Problem:**
```ruby
User.all.select do |user|
  user.posts.published.any?  # N+1!
end
```

**Solution:**
```ruby
# Use joins and where instead
User.joins(:posts).where(posts: { published: true }).distinct

# Or includes with filter
User.includes(:posts).where(posts: { published: true }).references(:posts)
```

### Pattern 5: JSON/API Responses

**Problem:**
```ruby
users = User.all
render json: users.map { |u|
  {
    name: u.name,
    posts: u.posts.map { |p| p.title }  # N+1
  }
}
```

**Solution:**
```ruby
users = User.includes(:posts)
render json: users.map { |u|
  {
    name: u.name,
    posts: u.posts.map { |p| p.title }  # Uses preloaded data
  }
}
```

## Eager Loading Strategies

### includes

Most common. Uses separate queries but loads all at once.

```ruby
User.includes(:posts)
# Query 1: SELECT * FROM users
# Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
```

**Use when:** You'll access the association

### preload

Always uses separate queries (like includes).

```ruby
User.preload(:posts)
```

**Use when:** You want to ensure separate queries (for query optimization)

### eager_load

Uses LEFT OUTER JOIN - single query.

```ruby
User.eager_load(:posts)
# SELECT * FROM users LEFT OUTER JOIN posts ON posts.user_id = users.id
```

**Use when:** You need WHERE conditions on associations

### joins

Uses INNER JOIN - doesn't load associations.

```ruby
User.joins(:posts)
```

**Use when:** You only need to filter/count, not access association data

## Comparison

| Method | Queries | Loads Association | Use Case |
|--------|---------|-------------------|----------|
| includes | 2 (or 1 with join) | Yes | Default choice |
| preload | 2+ | Yes | Force separate queries |
| eager_load | 1 (join) | Yes | Need WHERE on association |
| joins | 1 (join) | No | Filter only, don't need data |

## Counter Caches

For simple counts, use counter caches instead of queries:

```ruby
# Migration
class AddPostsCountToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :posts_count, :integer, default: 0
  end
end

# Model
class Post < ApplicationRecord
  belongs_to :user, counter_cache: true
end

# Now this is instant (no query):
user.posts_count  # Reads from column, not COUNT query
```

## Detection Process

1. **Scan for association access** in loops or maps
2. **Check if eager loading present** in the query
3. **Identify nested association access**
4. **Look for counts/sizes** on associations
5. **Check API/JSON serialization** code
6. **Review logs** for repeated similar queries

## Examples

### Example 1: Controller Action

**Code:**
```ruby
class UsersController < ApplicationController
  def index
    @users = User.where(active: true)
  end
end

# View: app/views/users/index.html.erb
<% @users.each do |user| %>
  <div>
    <h2><%= user.name %></h2>
    <p>Posts: <%= user.posts.count %></p>
    <p>Comments: <%= user.comments.count %></p>
  </div>
<% end %>
```

**Issue:** N+1 on both posts and comments

**Suggestion:**
"Add eager loading in the controller:
```ruby
@users = User.includes(:posts, :comments).where(active: true)
```

Or use counter caches for better performance."

### Example 2: API Endpoint

**Code:**
```ruby
def index
  users = User.all
  render json: users.map { |u|
    {
      id: u.id,
      name: u.name,
      recent_posts: u.posts.recent.limit(5).map(&:title)
    }
  }
end
```

**Issue:** N+1 on posts

**Suggestion:**
"Eager load the association:
```ruby
users = User.includes(:posts)
render json: users.map { |u|
  {
    id: u.id,
    name: u.name,
    recent_posts: u.posts.recent.limit(5).map(&:title)
  }
}
```

Note: =.recent= is called on the preloaded association in memory."

### Example 3: Complex Nested

**Code:**
```ruby
@blogs = Blog.includes(:posts)

@blogs.each do |blog|
  blog.posts.each do |post|
    post.comments.each do |comment|
      puts comment.author.name  # N+1 on authors!
    end
  end
end
```

**Issue:** Missing nested includes

**Suggestion:**
"Include the full nesting:
```ruby
@blogs = Blog.includes(posts: { comments: :author })
```

This loads all data in 4 queries instead of potentially hundreds."

## Tools for Detection

### Bullet Gem

The most popular N+1 detector for Rails.

```ruby
# Gemfile
group :development do
  gem 'bullet'
end

# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end
```

Bullet will notify you when:
- N+1 queries occur
- Unused eager loading (over-eager loading)
- Missing counter caches

### Prosopite

Alternative N+1 detector with different approach.

```ruby
# Gemfile
gem 'prosopite'

# In development
Prosopite.scan
# Your code here
Prosopite.finish
```

### n_plus_one_control

RSpec/Minitest matchers for testing N+1 in specs.

```ruby
# In spec
it "avoids N+1 queries" do
  expect { get :index }.to perform_constant_number_of_queries
end

# With tolerance
it "keeps queries constant" do
  populate { create_list(:user, 3) }

  expect do
    get :index
  end.to perform_constant_number_of_queries
end
```

### Rails Query Log Tags (Rails 7+)

Shows query origin in logs.

```ruby
# config/environments/development.rb
config.active_record.query_log_tags_enabled = true
```

Logs will show which line triggered each query.

## Best Practices

### Use .size Instead of .count

- =.count= always hits database
- =.size= uses preloaded data if available
- =.length= loads all records into memory

```ruby
# With includes
users = User.includes(:posts)

users.first.posts.count   # SELECT COUNT(*) - still hits DB
users.first.posts.size    # Uses preloaded data - no query
users.first.posts.length  # Uses preloaded data - no query
```

### Avoid Over-Eager Loading

Only include what you'll actually use:

```ruby
# Bad - loading associations you won't use
User.includes(:posts, :comments, :profile, :settings)

# Good - only what's needed
User.includes(:posts)  # If you only use posts
```

### Use Scopes for Common Patterns

```ruby
class User < ApplicationRecord
  scope :with_posts, -> { includes(:posts) }
  scope :with_associations, -> { includes(:posts, :profile) }
end

# Usage
User.with_posts.where(active: true)
```

### Consider Database-Level Solutions

For aggregations, use SQL:

```ruby
# Instead of loading all posts and counting in Ruby:
users_with_count = User.joins(:posts)
                       .select('users.*, COUNT(posts.id) as posts_count')
                       .group('users.id')
```

## When to Skip

N+1 queries aren't always bad:
- Small datasets (< 10 records)
- One-time scripts
- Background jobs processing one record at a time
- Complex filtering that's easier with separate queries

Always profile before optimizing.

## Offering Suggestions

When detecting N+1:

1. **Identify the association** being accessed
2. **Show the N+1 pattern** in the code
3. **Suggest the fix** with specific code
4. **Explain the impact** ("This could run 100+ queries")
5. **Mention tools** like Bullet for ongoing detection
6. **Offer to refactor** if user wants help

1.4. Rails Security Checker

Automatically detect common security vulnerabilities in Rails code.

---
name: Rails Security Checker
description: Automatically detect common security vulnerabilities in Rails code including SQL injection, mass assignment, CSRF issues, and strong parameters violations
allowed-tools:
  - Read
  - Grep
  - Bash(bundle:*)
  - Bash(bundle audit:*)
---

# Rails Security Checker

## Activation Triggers

Automatically activate when:
- Viewing controllers with =params= usage
- Database query construction with string interpolation
- User input handling (forms, APIs)
- Authentication or authorization code
- File upload handling
- HTML output with =html_safe= or =raw=
- User mentions "security", "vulnerability", or "safe"
- Reviewing code for production deployment

## Security Categories

### 1. Strong Parameters (Mass Assignment Protection)

Rails requires explicit parameter whitelisting to prevent mass assignment vulnerabilities.

**Vulnerable Code:**
```ruby
class UsersController < ApplicationController
  def create
    # DANGEROUS - accepts all params!
    @user = User.create(params[:user])
  end

  def update
    # DANGEROUS - permits all attributes!
    @user.update(params.require(:user).permit!)
  end
end
```

**Issue:** Attacker can set ANY attribute, including:
- =admin= flag
- =role= field
- =verified= status
- =user_id= (change ownership)

**Safe Code:**
```ruby
class UsersController < ApplicationController
  def create
    @user = User.create(user_params)
  end

  def update
    @user.update(user_params)
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :bio)
    # Explicitly list allowed attributes
  end
end
```

**Detection Pattern:**
- =params[:model]= passed directly to =create=, =update=, or =new=
- =.permit!= usage (permits everything)
- Sensitive attributes in permit list (=:admin=, =:role=, =:verified=)

### 2. SQL Injection

Never use string interpolation or concatenation in SQL queries.

**Vulnerable Code:**
```ruby
# DANGEROUS - SQL injection!
User.where("email = '#{params[:email]}'")
User.where("name = '#{params[:name]}' AND active = true")

# Also dangerous
User.where("id IN (#{params[:ids].join(',')})")
```

**Attack Example:**
```ruby
params[:email] = "' OR '1'='1"
User.where("email = '#{params[:email]}'")
# Becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
# Returns ALL users!
```

**Safe Code:**
```ruby
# Safe - parameterized query
User.where("email = ?", params[:email])
User.where("name = ? AND active = ?", params[:name], true)

# Safe - hash conditions (ActiveRecord escapes)
User.where(email: params[:email])
User.where(name: params[:name], active: true)

# Safe - for IN clauses
User.where(id: params[:ids])  # Rails handles array escaping
```

**Detection Pattern:**
- =#{...}= interpolation inside SQL strings
- String concatenation (=+= or =<<) in =where=, =find_by_sql=
- =params= or user input in raw SQL

### 3. CSRF Protection

Rails protects against Cross-Site Request Forgery by default. Ensure it's enabled.

**Check Points:**

**1. ApplicationController has CSRF protection:**
```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Should have one of these:
  protect_from_forgery with: :exception
  # or
  protect_from_forgery with: :null_session  # For APIs
end
```

**2. Forms include authenticity token:**
```erb
<!-- Good - form helpers include token automatically -->
<%= form_with model: @user do |f| %>
  <%= f.text_field :name %>
<% end %>

<!-- Bad - custom forms need manual token -->
<form action="/users" method="post">
  <!-- Missing CSRF token! -->
  <input name="name">
</form>

<!-- Good - manual token included -->
<form action="/users" method="post">
  <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
  <input name="name">
</form>
```

**3. API endpoints handle CSRF appropriately:**
```ruby
class Api::BaseController < ActionController::Base
  # APIs don't use cookies, so skip CSRF
  skip_before_action :verify_authenticity_token

  # But require token-based auth instead
  before_action :authenticate_api_token!
end
```

**Detection Pattern:**
- =skip_before_action :verify_authenticity_token= without API auth
- Custom forms without =form_authenticity_token=
- Missing =protect_from_forgery= in base controller

### 4. Authentication & Authorization

Always verify users are authenticated AND authorized.

**Missing Authentication:**
```ruby
# BAD - no authentication check
class UsersController < ApplicationController
  def destroy
    @user = User.find(params[:id])
    @user.destroy
    # Anyone can delete any user!
  end
end

# GOOD - require authentication
class UsersController < ApplicationController
  before_action :authenticate_user!  # Devise, or custom

  def destroy
    @user = User.find(params[:id])
    @user.destroy
  end
end
```

**Missing Authorization:**
```ruby
# BAD - authenticated but not authorized
class UsersController < ApplicationController
  before_action :authenticate_user!

  def destroy
    @user = User.find(params[:id])
    @user.destroy
    # Any logged-in user can delete ANY user!
  end
end

# GOOD - check authorization
class UsersController < ApplicationController
  before_action :authenticate_user!

  def destroy
    @user = User.find(params[:id])
    authorize @user  # Pundit

    # Or manual check:
    unless @user == current_user || current_user.admin?
      redirect_to root_path, alert: "Not authorized"
      return
    end

    @user.destroy
  end
end
```

**Detection Pattern:**
- Sensitive actions (=create=, =update=, =destroy=) without auth checks
- =User.find(params[:id])= without authorization
- Admin-only actions without role checks

### 5. File Upload Security

Validate and sanitize uploaded files.

**Vulnerable Code:**
```ruby
# BAD - no validation
def upload
  uploaded_file = params[:file]
  File.write("public/uploads/#{uploaded_file.original_filename}", uploaded_file.read)
end
```

**Issues:**
- No file type validation (can upload .exe, .php)
- No size validation (can upload huge files)
- Filename not sanitized (directory traversal: =../../etc/passwd=)
- Files served from =public/= (can execute if server misconfigured)

**Safe Code:**
```ruby
class AvatarUploader < CarrierWave::Uploader::Base
  # Whitelist file extensions
  def extension_whitelist
    %w[jpg jpeg gif png]
  end

  # Limit file size
  def size_range
    1..5.megabytes
  end

  # Sanitize filename
  def filename
    "#{secure_token}.#{file.extension}" if original_filename.present?
  end

  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) ||
      model.instance_variable_set(var, SecureRandom.uuid)
  end
end

# In model
class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploader

  # Validate file presence
  validates :avatar, presence: true
end
```

**Detection Pattern:**
- Direct file writes without validation
- =File.write= with user-provided filenames
- No extension whitelist
- No size limits
- Files saved in =public/= directory

### 6. XSS (Cross-Site Scripting) Prevention

Rails escapes HTML by default, but watch for unsafe patterns.

**Vulnerable Code:**
```ruby
# BAD - marks user input as safe HTML!
<%= raw @user.bio %>
<%= @user.description.html_safe %>

# BAD in controller
def show
  @html_content = params[:content].html_safe
end
```

**Attack:**
```ruby
params[:bio] = "<script>alert('XSS')</script>"
# With .html_safe or raw, this executes!
```

**Safe Code:**
```ruby
# GOOD - Rails escapes by default
<%= @user.bio %>
# <script> becomes &lt;script&gt; (harmless text)

# If you MUST allow HTML, sanitize it:
<%= sanitize @user.bio, tags: %w[b i u], attributes: %w[href] %>

# Use a gem for rich text:
has_rich_text :bio  # ActionText (Rails 6+)
```

**Detection Pattern:**
- =.html_safe= on user input
- =raw()= on user input
- User content in =<script>= tags
- User content in =href="javascript:..."=

### 7. Hardcoded Secrets

Never commit secrets to the repository.

**Vulnerable Code:**
```ruby
# BAD - hardcoded credentials
API_KEY = "sk_live_abc123..."
Database.connect("postgres://user:password@host/db")

# BAD - secrets in code
AWS.config(access_key: "AKIAIOSFODNN7EXAMPLE")
```

**Safe Code:**
```ruby
# Use Rails credentials
api_key = Rails.application.credentials.api_key

# Or environment variables
api_key = ENV['API_KEY']

# config/credentials.yml.enc (encrypted)
aws:
  access_key: <%= ENV['AWS_ACCESS_KEY'] %>
  secret_key: <%= ENV['AWS_SECRET_KEY'] %>
```

**Detection Pattern:**
- =password=, =secret=, =key= in string literals
- Long alphanumeric strings that look like tokens
- Database URLs with credentials in code

### 8. Timing Attacks on Comparisons

Use constant-time comparison for secrets.

**Vulnerable Code:**
```ruby
# BAD - timing attack possible
def valid_token?(token)
  token == SECRET_TOKEN
end
```

**Issue:** String comparison exits early on mismatch. Attacker can measure response time to guess token character-by-character.

**Safe Code:**
```ruby
# GOOD - constant-time comparison
def valid_token?(token)
  ActiveSupport::SecurityUtils.secure_compare(token, SECRET_TOKEN)
end
```

## Vulnerability Severity

When reporting issues, classify by severity:

**Critical (Fix Immediately):**
- SQL injection vulnerabilities
- Mass assignment of admin/role fields
- Authentication bypass
- Hardcoded credentials in code

**High (Fix Soon):**
- Missing authorization checks
- CSRF protection disabled
- File upload without validation
- XSS vulnerabilities

**Medium (Should Fix):**
- Missing authentication on non-critical endpoints
- Weak session configuration
- Information disclosure in error messages

**Low (Nice to Fix):**
- Missing security headers
- Verbose error messages
- Weak password requirements

## Detection Process

1. **Scan for SQL interpolation** - Grep for =#{...}= in =where= clauses
2. **Check params usage** - Look for =params[:model]= without =permit=
3. **Review authentication** - Check =before_action= filters
4. **Examine file uploads** - Verify validation and sanitization
5. **Check HTML output** - Find =html_safe= and =raw= usage
6. **Run bundle audit** - Check for vulnerable gem versions

## Tools Integration

### bundle-audit

Check for known vulnerabilities in gems:

```bash
gem install bundler-audit
bundle audit check --update
```

In skill, run:
```bash
Bash(bundle audit:*)
```

### Brakeman

Static analysis security scanner for Rails:

```ruby
# Gemfile
group :development do
  gem 'brakeman'
end

# Run
bundle exec brakeman
```

### RuboCop Security

Security-focused RuboCop rules:

```ruby
# .rubocop.yml
require:
  - rubocop-rails

Security:
  Enabled: true
```

## Examples

### Example 1: Mass Assignment

**User Code:**
```ruby
def update
  @post = Post.find(params[:id])
  @post.update(params[:post])
end
```

**Issue:** Mass assignment vulnerability (Critical)

**Suggestion:**
"This allows users to update ANY field on the post, including =user_id= (changing ownership) or =published= status. Use strong parameters:

```ruby
def update
  @post = Post.find(params[:id])
  @post.update(post_params)
end

private

def post_params
  params.require(:post).permit(:title, :body)
end
```"

### Example 2: SQL Injection

**User Code:**
```ruby
def search
  @users = User.where("name LIKE '%#{params[:query]}%'")
end
```

**Issue:** SQL injection (Critical)

**Suggestion:**
"This is vulnerable to SQL injection. An attacker could use =params[:query] = "'; DROP TABLE users; --"= to destroy data. Use parameterized queries:

```ruby
def search
  @users = User.where('name LIKE ?', "%#{params[:query]}%")
end
```"

### Example 3: Missing Authorization

**User Code:**
```ruby
class PostsController < ApplicationController
  before_action :authenticate_user!

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
    redirect_to posts_path
  end
end
```

**Issue:** Missing authorization (High)

**Suggestion:**
"Users can delete ANY post, not just their own. Add authorization:

```ruby
def destroy
  @post = current_user.posts.find(params[:id])
  # Or use Pundit:
  @post = Post.find(params[:id])
  authorize @post

  @post.destroy
  redirect_to posts_path
end
```"

## Best Practices

### Defense in Depth

Layer multiple security measures:
1. Input validation (strong parameters)
2. Authorization checks (Pundit, CanCanCan)
3. Prepared statements (parameterized queries)
4. Output escaping (Rails default)
5. Security headers (Secure Headers gem)

### Principle of Least Privilege

- Only permit necessary parameters
- Grant minimum required permissions
- Limit database user permissions
- Restrict file system access

### Fail Securely

When security checks fail:
- Log the attempt
- Return generic error (don't reveal details)
- Redirect to safe page
- Don't expose stack traces in production

### Keep Dependencies Updated

```bash
bundle update --security
bundle audit check
```

Run regularly and fix vulnerabilities promptly.

## Security Headers

Ensure proper security headers in Rails:

```ruby
# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
  config.x_frame_options = "DENY"
  config.x_content_type_options = "nosniff"
  config.x_xss_protection = "1; mode=block"
  config.x_download_options = "noopen"
  config.x_permitted_cross_domain_policies = "none"
  config.referrer_policy = "strict-origin-when-cross-origin"
  config.csp = {
    default_src: %w['self'],
    script_src: %w['self']
  }
end
```

## Offering Suggestions

When security issues found:

1. **Clearly state the vulnerability** and its severity
2. **Explain the attack scenario** (what could go wrong)
3. **Provide secure code** as a drop-in replacement
4. **Reference Rails Security Guide** for more context
5. **Suggest tools** (Brakeman, bundle-audit)
6. **Offer to fix** if user wants help

Always be clear that security is critical and fixes should be prioritized.

## Resources

- [[https://guides.rubyonrails.org/security.html][Rails Security Guide]] - Official security best practices
- [[https://github.com/presidentbeef/brakeman][Brakeman]] - Static security analyzer
- [[https://github.com/rubysec/bundler-audit][bundler-audit]] - Check gem vulnerabilities
- [[https://owasp.org/www-project-top-ten/][OWASP Top 10]] - Common web vulnerabilities

1.5. Test Coverage Assistant (Rails)

Analyze Rails-specific test coverage including RSpec, Minitest, and FactoryBot patterns.

This skill extends the generic [BROKEN LINK: *Test Coverage Analyzer] with Rails-specific patterns.

---
name: Test Coverage Assistant (Rails)
description: Analyze Rails-specific test coverage including RSpec request/system/model specs, FactoryBot factories, and controller testing patterns
allowed-tools:
  - Read
  - Grep
  - Bash(bundle exec rspec:*)
  - Bash(bundle exec rake:*)
---

# Test Coverage Assistant (Rails)

## Activation Triggers

Automatically activate when:
- New Rails controllers or models without corresponding tests
- Modified Rails code without test updates
- User mentions "testing", "RSpec", "Minitest", or "specs"
- Running test commands (=rspec=, =rails test=)
- Test files created or modified
- User asks about test structure or coverage

## Rails Test Types

### 1. Model Specs (RSpec)

Test ActiveRecord models including validations, associations, scopes, and business logic.

**Test Coverage Checklist:**
- [ ] Validations (presence, uniqueness, format, length, numericality)
- [ ] Associations (has_many, belongs_to, has_one)
- [ ] Scopes and class methods
- [ ] Instance methods
- [ ] Callbacks (before_save, after_create, etc.)
- [ ] Custom validators

**Example:**
```ruby
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:email) }
    it { should validate_uniqueness_of(:email).case_insensitive }
    it { should validate_length_of(:name).is_at_least(2) }
  end

  describe 'associations' do
    it { should have_many(:posts).dependent(:destroy) }
    it { should belong_to(:organization) }
  end

  describe 'scopes' do
    it 'returns active users' do
      active = create(:user, active: true)
      inactive = create(:user, active: false)

      expect(User.active).to include(active)
      expect(User.active).not_to include(inactive)
    end
  end

  describe '#full_name' do
    it 'combines first and last name' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end
  end
end
```

### 2. Request Specs (RSpec)

Test full HTTP request/response cycle including routing, controller, and view rendering.

**Test Coverage Checklist:**
- [ ] Each route (GET, POST, PATCH, DELETE)
- [ ] Request parameters handling
- [ ] HTTP status codes
- [ ] Response format (HTML, JSON, etc.)
- [ ] Authentication requirements
- [ ] Authorization checks
- [ ] Error handling

**Example:**
```ruby
# spec/requests/users_spec.rb
RSpec.describe "Users API", type: :request do
  describe "GET /users" do
    it "returns a list of users" do
      create_list(:user, 3)

      get users_path
      expect(response).to have_http_status(:success)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe "POST /users" do
    context "with valid params" do
      let(:valid_params) { { user: attributes_for(:user) } }

      it "creates a new user" do
        expect {
          post users_path, params: valid_params
        }.to change(User, :count).by(1)
      end

      it "returns created status" do
        post users_path, params: valid_params
        expect(response).to have_http_status(:created)
      end
    end

    context "with invalid params" do
      let(:invalid_params) { { user: { email: '' } } }

      it "does not create a user" do
        expect {
          post users_path, params: invalid_params
        }.not_to change(User, :count)
      end

      it "returns unprocessable entity status" do
        post users_path, params: invalid_params
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end

  describe "PATCH /users/:id" do
    let(:user) { create(:user) }

    it "updates the user" do
      patch user_path(user), params: { user: { name: 'New Name' } }
      expect(user.reload.name).to eq('New Name')
    end
  end

  describe "DELETE /users/:id" do
    let!(:user) { create(:user) }

    it "deletes the user" do
      expect {
        delete user_path(user)
      }.to change(User, :count).by(-1)
    end
  end
end
```

### 3. System Specs (Feature Tests)

Test full user workflows from browser perspective using Capybara.

**Test Coverage Checklist:**
- [ ] Complete user workflows
- [ ] Form submissions
- [ ] Link navigation
- [ ] JavaScript interactions
- [ ] Error messages displayed
- [ ] Success messages displayed

**Example:**
```ruby
# spec/system/user_registration_spec.rb
RSpec.describe "User Registration", type: :system do
  before do
    driven_by(:rack_test)
  end

  it "allows user to register" do
    visit new_user_registration_path

    fill_in "Email", with: "user@example.com"
    fill_in "Password", with: "password123"
    fill_in "Password confirmation", with: "password123"

    click_button "Sign up"

    expect(page).to have_content("Welcome! You have signed up successfully")
    expect(page).to have_current_path(root_path)
  end

  it "shows errors for invalid registration" do
    visit new_user_registration_path

    fill_in "Email", with: ""
    click_button "Sign up"

    expect(page).to have_content("Email can't be blank")
  end
end
```

### 4. Controller Tests (Minitest)

For projects using Minitest instead of RSpec.

**Example:**
```ruby
# test/controllers/users_controller_test.rb
class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get users_url
    assert_response :success
  end

  test "should create user" do
    assert_difference('User.count') do
      post users_url, params: { user: { name: "John", email: "john@example.com" } }
    end

    assert_redirected_to user_url(User.last)
  end

  test "should update user" do
    user = users(:one)
    patch user_url(user), params: { user: { name: "Updated Name" } }

    assert_redirected_to user_url(user)
    user.reload
    assert_equal "Updated Name", user.name
  end

  test "should destroy user" do
    user = users(:one)
    assert_difference('User.count', -1) do
      delete user_url(user)
    end

    assert_redirected_to users_url
  end
end
```

## FactoryBot Patterns

FactoryBot is the standard fixture replacement for Rails (used by 70% of Rails developers).

### Basic Factory

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "John Doe" }
    sequence(:email) { |n| "user#{n}@example.com" }
    password { "password123" }
    active { true }
  end
end

# Usage
user = create(:user)  # Persists to database
user = build(:user)   # Builds object, doesn't save
user = build_stubbed(:user)  # Stubbed object (fastest)
attrs = attributes_for(:user)  # Hash of attributes
```

### Traits

Use traits for variations:

```ruby
FactoryBot.define do
  factory :user do
    name { "John Doe" }
    email { Faker::Internet.email }

    trait :admin do
      role { :admin }
    end

    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end

    trait :inactive do
      active { false }
    end
  end
end

# Usage
admin = create(:user, :admin)
user_with_posts = create(:user, :with_posts)
inactive_admin = create(:user, :admin, :inactive)
```

### Associations

```ruby
FactoryBot.define do
  factory :post do
    title { "Sample Post" }
    body { "Lorem ipsum..." }
    association :user  # Creates associated user

    # Or explicit:
    user { association :user, :admin }
  end
end
```

### Sequences

```ruby
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    sequence(:username) { |n| "user#{n}" }
  end
end
```

## Test Structure Best Practices

### RSpec Structure

```ruby
RSpec.describe User, type: :model do
  # Use describe for methods
  describe '#full_name' do
    # Use context for different states
    context 'when first and last name present' do
      it 'returns full name' do
        # Arrange
        user = build(:user, first_name: 'John', last_name: 'Doe')

        # Act
        result = user.full_name

        # Assert
        expect(result).to eq('John Doe')
      end
    end

    context 'when only first name present' do
      it 'returns first name' do
        user = build(:user, first_name: 'John', last_name: nil)
        expect(user.full_name).to eq('John')
      end
    end
  end

  describe '.active' do
    it 'returns only active users' do
      active_user = create(:user, active: true)
      inactive_user = create(:user, active: false)

      expect(User.active).to include(active_user)
      expect(User.active).not_to include(inactive_user)
    end
  end
end
```

### Use let for Test Data

```ruby
RSpec.describe PostsController do
  # let is lazy-evaluated (only when called)
  let(:user) { create(:user) }
  let(:post) { create(:post, user: user) }

  # let! is eagerly evaluated (created before each test)
  let!(:published_posts) { create_list(:post, 3, :published) }

  describe "GET #index" do
    it "assigns published posts" do
      get :index
      expect(assigns(:posts)).to match_array(published_posts)
    end
  end
end
```

### Shared Examples

```ruby
# spec/support/shared_examples/authenticatable.rb
RSpec.shared_examples "authenticatable" do
  it "requires authentication" do
    get :index
    expect(response).to redirect_to(login_path)
  end
end

# In spec
RSpec.describe AdminController do
  it_behaves_like "authenticatable"
end
```

## Coverage Analysis

### Running Coverage Reports

**With SimpleCov (RSpec):**
```ruby
# spec/spec_helper.rb
require 'simplecov'
SimpleCov.start 'rails' do
  add_filter '/spec/'
  add_filter '/config/'
  add_group 'Controllers', 'app/controllers'
  add_group 'Models', 'app/models'
  add_group 'Services', 'app/services'
end

# Run tests
bundle exec rspec

# View coverage: open coverage/index.html
```

**With Rails default (Minitest):**
```bash
COVERAGE=true rails test
```

### What to Test

**High Priority:**
- Models: All validations, associations, important methods
- Controllers/Requests: All CRUD actions, authentication/authorization
- Critical business logic
- API endpoints
- User-facing workflows

**Medium Priority:**
- Helper methods
- Service objects
- Background jobs
- Mailers

**Low Priority (Can Skip):**
- Simple getters/setters
- Obvious delegations
- Generated code (devise controllers, etc.)

## Example: Missing Coverage Detection

**Scenario:** User adds new controller without tests

**Code:**
```ruby
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
  def index
    @posts = Post.all
    render json: @posts
  end

  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      render json: @post, status: :created
    else
      render json: @post.errors, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body)
  end
end
```

**Detection:** No corresponding test file at =spec/requests/api/v1/posts_spec.rb=

**Suggestion:**
"I notice this controller doesn't have request specs. I recommend adding:

```ruby
# spec/requests/api/v1/posts_spec.rb
RSpec.describe 'Api::V1::Posts', type: :request do
  describe 'GET /api/v1/posts' do
    it 'returns all posts as JSON' do
      create_list(:post, 3)

      get '/api/v1/posts'

      expect(response).to have_http_status(:success)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /api/v1/posts' do
    let(:user) { create(:user) }
    let(:valid_params) { { post: { title: 'Test', body: 'Content' } } }

    before { sign_in user }

    context 'with valid params' do
      it 'creates a new post' do
        expect {
          post '/api/v1/posts', params: valid_params
        }.to change(Post, :count).by(1)
      end

      it 'returns created status' do
        post '/api/v1/posts', params: valid_params
        expect(response).to have_http_status(:created)
      end
    end

    context 'with invalid params' do
      let(:invalid_params) { { post: { title: '' } } }

      it 'returns unprocessable entity' do
        post '/api/v1/posts', params: invalid_params
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end
end
```

Would you like me to create these tests?"

## Tools

### RSpec

```bash
bundle exec rspec  # Run all specs
bundle exec rspec spec/models  # Run model specs only
bundle exec rspec spec/models/user_spec.rb:10  # Run specific line
```

### Minitest

```bash
rails test  # Run all tests
rails test test/models  # Run model tests
rails test test/models/user_test.rb:10  # Run specific test
```

### Guard

Auto-run tests on file changes:

```ruby
# Gemfile
group :development do
  gem 'guard-rspec'
end

# Run
bundle exec guard
```

## Offering Suggestions

When detecting missing coverage:

1. **Identify what's untested** (model, controller action, service)
2. **Explain why it's important** ("This endpoint handles payments...")
3. **Suggest specific test cases** (happy path, edge cases, errors)
4. **Provide complete test code** ready to use
5. **Mention coverage goals** (aim for 80%+ on critical code)
6. **Offer to generate tests** if user wants

Be helpful but not pushy - respect if user declines.

1.6. Migration Safety Checker

Detect dangerous migration operations that could cause downtime or data loss.

---
name: Migration Safety Checker
description: Detect dangerous migration operations that could cause downtime or data loss, based on strong_migrations gem patterns and PostgreSQL best practices
allowed-tools:
  - Read
  - Grep
  - Bash(bundle exec rails:*)
---

# Migration Safety Checker

## Activation Triggers

Automatically activate when:
- Creating or editing migration files (=db/migrate/=)
- User runs =rails db:migrate= or =rails db:rollback=
- Mentions of "database changes", "schema", or "migration"
- Production deployment preparation
- User asks about migration safety

## Dangerous Operations

### 1. Adding Column with Default Value

**Problem:**
```ruby
# DANGEROUS - locks table for entire rewrite!
class AddAdminToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
```

**Why Dangerous:**
- PostgreSQL < 11: Rewrites entire table, holding exclusive lock
- Large tables: Can lock for minutes or hours
- Blocks all reads and writes during migration
- Can cause cascading failures

**Safe Alternative:**
```ruby
# Step 1: Add column without default
class AddAdminToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :admin, :boolean
  end
end

# Step 2: Backfill in batches (separate migration or rake task)
class BackfillUserAdmin < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    User.in_batches.update_all(admin: false)
  end
end

# Step 3: Add default
class SetAdminDefault < ActiveRecord::Migration[7.0]
  def change
    change_column_default :users, :admin, false
  end
end
```

**PostgreSQL 11+ Only:**
```ruby
# Safe on PostgreSQL 11+, dangerous on earlier versions
add_column :users, :admin, :boolean, default: false
```

### 2. Changing Column Type

**Problem:**
```ruby
# DANGEROUS - rewrites table, locks it
class ChangeEmailType < ActiveRecord::Migration[7.0]
  def change
    change_column :users, :email, :text
  end
end
```

**Why Dangerous:**
- Rewrites every row in table
- Holds exclusive lock during rewrite
- Can take hours on large tables

**Safe Alternative:**
```ruby
# Step 1: Add new column
class AddEmailNew < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :email_new, :text
  end
end

# Step 2: Dual-write (in application code)
# Model temporarily writes to both columns

# Step 3: Backfill data in batches
class BackfillEmailNew < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    User.find_in_batches do |batch|
      batch.each do |user|
        user.update_column(:email_new, user.email)
      end
    end
  end
end

# Step 4: Swap columns
class SwapEmailColumns < ActiveRecord::Migration[7.0]
  def change
    safety_assured do
      remove_column :users, :email
      rename_column :users, :email_new, :email
    end
  end
end
```

### 3. Removing Column

**Problem:**
```ruby
# DANGEROUS - causes errors if code still references column
class RemoveNameFromUsers < ActiveRecord::Migration[7.0]
  def change
    remove_column :users, :name
  end
end
```

**Why Dangerous:**
- ActiveRecord caches schema on app start
- Running app instances will try to use removed column
- =INSERT= and =UPDATE= statements will fail
- Can cause app-wide errors until all servers restart

**Safe Alternative:**
```ruby
# Step 1: Ignore column in model (deploy this first!)
class User < ApplicationRecord
  self.ignored_columns = ["name"]
end

# Deploy code, wait for all servers to restart

# Step 2: Remove column (separate deployment)
class RemoveNameFromUsers < ActiveRecord::Migration[7.0]
  def change
    safety_assured { remove_column :users, :name }
  end
end
```

### 4. Adding Index Without Concurrency

**Problem:**
```ruby
# DANGEROUS on PostgreSQL - locks table for writes
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email
  end
end
```

**Why Dangerous:**
- PostgreSQL requires =SHARE= lock to create index
- Blocks writes (=INSERT=, =UPDATE=, =DELETE=) during index creation
- Can take minutes on large tables
- Blocks all writes for the entire duration

**Safe Alternative:**
```ruby
# Safe - creates index concurrently (PostgreSQL 8.2+)
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!  # Required for concurrent indexes

  def change
    add_index :users, :email, algorithm: :concurrently
  end
end
```

**Note:** Concurrent indexes:
- Don't block writes
- Take longer to create
- Require =disable_ddl_transaction!=
- Can't be rolled back automatically

### 5. Adding Foreign Key

**Problem:**
```ruby
# DANGEROUS - validates all existing data, holds lock
class AddForeignKey < ActiveRecord::Migration[7.0]
  def change
    add_foreign_key :posts, :users
  end
end
```

**Why Dangerous:**
- Validates every row in table
- Holds lock during validation
- Can take minutes on large tables

**Safe Alternative:**
```ruby
# Add FK without validation, then validate separately
class AddForeignKey < ActiveRecord::Migration[7.0]
  def change
    add_foreign_key :posts, :users, validate: false
  end
end

class ValidateForeignKey < ActiveRecord::Migration[7.0]
  def change
    validate_foreign_key :posts, :users
  end
end
```

### 6. Renaming Column

**Problem:**
```ruby
# DANGEROUS - breaks running code
class RenameUserName < ActiveRecord::Migration[7.0]
  def change
    rename_column :users, :name, :full_name
  end
end
```

**Why Dangerous:**
- Running app expects old column name
- All queries referencing =name= will fail immediately

**Safe Alternative:**
```ruby
# Step 1: Add new column
add_column :users, :full_name, :string

# Step 2: Dual-write (update model to write to both)
# Step 3: Backfill
User.find_in_batches { |batch| batch.update_all("full_name = name") }

# Step 4: Update code to read from new column
# Step 5: Remove old column (after deploy)
remove_column :users, :name
```

### 7. Renaming Table

**Problem:**
```ruby
# DANGEROUS - breaks running code
rename_table :users, :accounts
```

**Safe Alternative:**
- Create new table
- Dual-write to both
- Backfill data
- Switch reads to new table
- Remove old table

Or use database views as an intermediate step.

### 8. Adding NOT NULL Constraint

**Problem:**
```ruby
# DANGEROUS - fails if any NULL values exist
class AddNotNullToEmail < ActiveRecord::Migration[7.0]
  def change
    change_column_null :users, :email, false
  end
end
```

**Safe Alternative:**
```ruby
# Step 1: Add constraint with validation (PostgreSQL 12+)
class AddNotNullConstraint < ActiveRecord::Migration[7.0]
  def change
    add_check_constraint :users, "email IS NOT NULL",
                        name: "users_email_null",
                        validate: false
  end
end

# Step 2: Validate constraint (doesn't block writes)
class ValidateNotNullConstraint < ActiveRecord::Migration[7.0]
  def change
    validate_check_constraint :users, name: "users_email_null"
  end
end

# Step 3: Add NOT NULL (now safe, constraint already validated)
class SafelyAddNotNull < ActiveRecord::Migration[7.0]
  def change
    change_column_null :users, :email, false
    remove_check_constraint :users, name: "users_email_null"
  end
end
```

## Detection Process

1. **Read migration file** - Check for dangerous methods
2. **Identify table size** - Large tables = higher risk
3. **Check for safety patterns** - =disable_ddl_transaction!=, =algorithm: :concurrently=
4. **Classify severity** - Based on table size and lock duration
5. **Suggest safe alternatives** - Provide step-by-step guidance

## Vulnerability Severity

**Critical (Never Do in Production):**
- Adding column with default on large table (PostgreSQL < 11)
- Changing column type on large table
- Renaming table or column without dual-write period
- Adding index without =:concurrently= on large table

**High (Dangerous, Needs Care):**
- Removing column without ignoring first
- Adding foreign key without =validate: false=
- Adding NOT NULL without constraint validation

**Medium (Can Cause Issues):**
- Running long migrations in transaction
- Backfilling without batching
- Not setting lock timeout

**Low (Best Practice):**
- Missing reversibility (=def change= vs =def up/down=)
- Not using =safety_assured= when intentionally using dangerous operation

## Strong Migrations Integration

The =strong_migrations= gem detects these issues automatically.

**Installation:**
```ruby
# Gemfile
gem 'strong_migrations'

# Install
bundle install
rails generate strong_migrations:install
```

**Configuration:**
```ruby
# config/initializers/strong_migrations.rb
StrongMigrations.start_after = 20200101000000  # Skip old migrations

StrongMigrations.target_version = 7.0  # Target Rails version

# Customize checks
StrongMigrations.check_down = false  # Don't check down methods
StrongMigrations.auto_analyze = true  # Auto-analyze after adding indexes
```

**Usage:**
```ruby
# Strong migrations will raise error on dangerous operation
add_column :users, :admin, :boolean, default: false
# => Raises StrongMigrations::UnsafeMigration

# If you know what you're doing, bypass check:
safety_assured do
  add_column :users, :admin, :boolean, default: false
end
```

## Best Practices

### 1. Set Lock Timeout

Prevent migrations from blocking indefinitely:

```ruby
class AddIndex < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    # Fail after 5 seconds instead of waiting forever
    execute "SET lock_timeout = '5s'"
    add_index :users, :email, algorithm: :concurrently
  end

  def down
    remove_index :users, :email
  end
end
```

### 2. Batch Large Updates

Don't update millions of rows at once:

```ruby
class BackfillData < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    User.find_in_batches(batch_size: 1000) do |batch|
      batch.update_all(role: 'user')
      sleep 0.01  # Give database a break
    end
  end
end
```

### 3. Test Migrations Locally First

```bash
# On a copy of production data
rails db:migrate:status
rails db:migrate
rails db:rollback
rails db:migrate
```

### 4. Use Reversible Migrations

```ruby
class AddIndex < ActiveRecord::Migration[7.0]
  def up
    add_index :users, :email
  end

  def down
    remove_index :users, :email
  end
end
```

### 5. Monitor Migration Duration

```bash
# Time the migration
time rails db:migrate
```

## Example Detections

### Example 1: Unsafe Default

**User Code:**
```ruby
class AddStatusToOrders < ActiveRecord::Migration[7.0]
  def change
    add_column :orders, :status, :string, default: 'pending'
  end
end
```

**Issue:** Adding column with default (Critical on PostgreSQL < 11)

**Suggestion:**
"This migration will lock the =orders= table during a full table rewrite, which could take minutes and block all reads/writes.

Use this safer 3-step approach:

```ruby
# Migration 1: Add column without default
class AddStatusToOrders < ActiveRecord::Migration[7.0]
  def change
    add_column :orders, :status, :string
  end
end

# Migration 2: Backfill in batches
class BackfillOrderStatus < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    Order.in_batches.update_all(status: 'pending')
  end
end

# Migration 3: Set default
class SetStatusDefault < ActiveRecord::Migration[7.0]
  def change
    change_column_default :orders, :status, 'pending'
  end
end
```

Or if you're on PostgreSQL 11+, this is safe:
```ruby
add_column :orders, :status, :string, default: 'pending'
```"

### Example 2: Missing Concurrent Index

**User Code:**
```ruby
class AddIndexToPostsUserId < ActiveRecord::Migration[7.0]
  def change
    add_index :posts, :user_id
  end
end
```

**Issue:** Missing =algorithm: :concurrently= (Critical on large tables)

**Suggestion:**
"This index creation will lock the =posts= table for writes until the index is built. Use concurrent index creation:

```ruby
class AddIndexToPostsUserId < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_index :posts, :user_id, algorithm: :concurrently
  end
end
```

Note: Concurrent indexes take longer to build but don't block writes."

### Example 3: Removing Column

**User Code:**
```ruby
class RemoveAgeFromUsers < ActiveRecord::Migration[7.0]
  def change
    remove_column :users, :age
  end
end
```

**Issue:** Removing column without ignoring first (High)

**Suggestion:**
"Removing a column immediately will cause errors in running app instances that still reference it.

Use this 2-step process:

```ruby
# Step 1: Ignore column in model (deploy first)
class User < ApplicationRecord
  self.ignored_columns = ['age']
end

# Deploy and restart all app servers

# Step 2: Remove column (separate deployment)
class RemoveAgeFromUsers < ActiveRecord::Migration[7.0]
  def change
    safety_assured { remove_column :users, :age }
  end
end
```"

## When to Use Safety_assured

Only use =safety_assured= when you:
- Understand the risk
- Have verified it's safe (e.g., small table)
- Are following multi-step process for dangerous operation
- Need to remove column after ignoring it

```ruby
# OK - you've already ignored the column
safety_assured do
  remove_column :users, :deprecated_field
end

# OK - small lookup table
safety_assured do
  add_column :statuses, :color, :string, default: 'gray'
end

# NOT OK - bypassing safety on large table
safety_assured do
  add_column :orders, :total, :decimal, default: 0.0  # BAD!
end
```

## Resources

- [[https://github.com/ankane/strong_migrations][strong_migrations gem]] - Catches unsafe migrations
- [[https://github.com/fatkodima/online_migrations][online_migrations gem]] - Advanced PostgreSQL safety
- [[https://www.postgresql.org/docs/current/sql-altertable.html][PostgreSQL ALTER TABLE docs]] - Understanding locks
- [[https://guides.rubyonrails.org/active_record_migrations.html][Rails Migrations Guide]] - Official documentation

1.7. Rails Upgrade Helper

Detect deprecated Rails patterns and suggest modern alternatives when upgrading Rails versions.

---
name: Rails Upgrade Helper
description: Detect deprecated Rails patterns and version-specific issues when upgrading Rails versions, suggesting modern replacements
allowed-tools:
  - Read
  - Grep
  - Bash(bundle:*)
  - Bash(rails:*)
---

# Rails Upgrade Helper

## Activation Triggers

Automatically activate when:
- =Gemfile= shows Rails version changes
- User mentions "upgrading Rails", "Rails upgrade", or specific versions
- Deprecated pattern detected in code
- User viewing Rails configuration files
- User asks about Rails version differences

## Version-Specific Changes

### Rails 7 → Rails 7.2

**Key Changes:**
- =@rails/ujs= removed (use Turbo instead)
- Positional =coder= argument deprecated in =serialize()=
- Plural association name references deprecated
- =ConnectionPool#connection= → =ConnectionPool#lease_connection=

**Example:**
```ruby
# Old (Rails 7.0)
serialize :options, Hash

# New (Rails 7.2)
serialize :options, coder: Hash  # Or type: Hash in Rails 7.1+
```

### Rails 7.2 → Rails 8

**Key Changes:**
- =config.active_job.use_big_decimal_serializer= removed
- Deprecated config files removed
- Parameters hash equality configuration removed
- Action* classes simplified

### Rails 6 → Rails 7

**Major Changes:**
- Zeitwerk autoloading required (was optional in Rails 6)
- =before_action :verify_authenticity_token= → implicit via =protect_from_forgery=
- Import maps replace Webpacker by default
- ActiveStorage variants API updated
- Turbo replaces Turbolinks
- CSS bundling changes

**Example:**
```ruby
# Old (Rails 6 with Zeitwerk)
# config/application.rb
config.load_defaults 6.1
config.autoloader = :zeitwerk  # Optional

# New (Rails 7)
config.load_defaults 7.0
# Zeitwerk is mandatory, no config needed
```

## Common Deprecations

### ActiveRecord Query Syntax

**find_by_* Methods:**
```ruby
# Old (deprecated since Rails 4)
User.find_by_email("user@example.com")
User.find_all_by_status("active")

# New
User.find_by(email: "user@example.com")
User.where(status: "active")
```

**Dynamic Finders:**
```ruby
# Old
User.find_or_create_by_email("user@example.com")

# New
User.find_or_create_by(email: "user@example.com")
```

### Callback Syntax

**Proc.new → Lambda:**
```ruby
# Old
before_save :method, if: Proc.new { |user| user.email_changed? }

# New
before_save :method, if: -> { email_changed? }
# Or
before_save :method, if: :email_changed?
```

### Controller Filters

**before_filter → before_action:**
```ruby
# Old (deprecated since Rails 4)
class ApplicationController < ActionController::Base
  before_filter :authenticate_user!
  after_filter :log_activity
end

# New
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  after_action :log_activity
end
```

### Iteration Methods

**all.each → find_each:**
```ruby
# Old (loads all records into memory)
User.all.each do |user|
  user.send_notification
end

# New (batched iteration)
User.find_each do |user|
  user.send_notification
end

# Or with batch size
User.find_each(batch_size: 500) do |user|
  user.send_notification
end
```

### Mass Assignment (Pre-Rails 4)

**attr_accessible → Strong Parameters:**
```ruby
# Old (Rails 3)
class User < ActiveRecord::Base
  attr_accessible :name, :email
end

# New (Rails 4+)
class User < ApplicationRecord
  # No attr_accessible
end

# In controller
def user_params
  params.require(:user).permit(:name, :email)
end
```

### Asset Pipeline

**Rails 6 → Rails 7:**
```ruby
# Old (Sprockets)
# app/assets/javascripts/application.js
//= require rails-ujs
//= require turbolinks
//= require_tree .

# New (Import Maps - Rails 7+)
# config/importmap.rb
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
```

### Autoloading

**Classic → Zeitwerk:**
```ruby
# Old (Classic autoloader)
# Files could be in any case
app/models/my_model.rb → MyModel  # Works
app/models/MyModel.rb → MyModel   # Also works

# New (Zeitwerk - strict naming)
app/models/my_model.rb → MyModel  # Correct
app/models/MyModel.rb → MyModel   # ERROR - filename must be snake_case
```

## Detection Patterns

### Pattern 1: Deprecated Methods

**Detection:**
```ruby
# Grep for deprecated patterns
Grep(pattern: "find_by_\\w+", glob: "**/*.rb")
Grep(pattern: "find_all_by_\\w+", glob: "**/*.rb")
Grep(pattern: "before_filter", glob: "**/*.rb")
Grep(pattern: "after_filter", glob: "**/*.rb")
Grep(pattern: "\\.all\\.each", glob: "**/*.rb")
```

### Pattern 2: Rails Version in Gemfile

```ruby
# Read Gemfile to check Rails version
Read("Gemfile")

# Look for:
gem 'rails', '~> 7.0'  # Current version
```

### Pattern 3: Configuration Files

Check =config/application.rb= for:
- =config.load_defaults= version
- Autoloader configuration
- Deprecated settings

## Upgrade Process Guidance

### Step-by-Step Upgrade

**1. Update Rails version incrementally**
```ruby
# Don't jump versions (6.0 → 8.0)
# Go step by step (6.0 → 6.1 → 7.0 → 7.1 → 7.2 → 8.0)

# Gemfile
gem 'rails', '~> 7.0.0'  # Specify exact minor version
```

**2. Update config**
```ruby
# config/application.rb
config.load_defaults 7.0  # Update to new version
```

**3. Check deprecation warnings**
```bash
# Run tests and check logs for deprecation warnings
bundle exec rspec
# Look for DEPRECATION WARNING in output
```

**4. Fix deprecations**
- Address all deprecation warnings before upgrading to next version
- Use this skill to identify and fix patterns

**5. Update gems**
```bash
bundle update
# Check for gem compatibility issues
```

**6. Test thoroughly**
- Run full test suite
- Test in staging environment
- Check for breaking changes

## Example Detections

### Example 1: Dynamic Finder

**User Code:**
```ruby
user = User.find_by_email(params[:email])
```

**Issue:** Deprecated dynamic finder (since Rails 4)

**Suggestion:**
"The =find_by_*= method style is deprecated. Use =find_by= with hash:

```ruby
user = User.find_by(email: params[:email])
```

This is more explicit and supports complex queries:
```ruby
user = User.find_by(email: params[:email], active: true)
```"

### Example 2: before_filter

**User Code:**
```ruby
class ApplicationController < ActionController::Base
  before_filter :authenticate_user!
end
```

**Issue:** =before_filter= deprecated (since Rails 4)

**Suggestion:**
"Use =before_action= instead of =before_filter=:

```ruby
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
end
```

Same for =after_filter= → =after_action= and =around_filter= → =around_action="

### Example 3: Iterating All Records

**User Code:**
```ruby
User.all.each do |user|
  UserMailer.welcome_email(user).deliver_later
end
```

**Issue:** Loading all records into memory (inefficient)

**Suggestion:**
"Use =find_each= for batched iteration:

```ruby
User.find_each do |user|
  UserMailer.welcome_email(user).deliver_later
end
```

This loads records in batches of 1000 (configurable), preventing memory issues on large datasets."

### Example 4: Zeitwerk Naming Violation

**User Code:**
```ruby
# File: app/models/MyUser.rb
class MyUser < ApplicationRecord
end
```

**Issue:** Filename doesn't match Zeitwerk convention

**Suggestion:**
"Zeitwerk requires snake_case filenames matching the class name:

File should be: =app/models/my_user.rb=

```ruby
# app/models/my_user.rb
class MyUser < ApplicationRecord
end
```

Zeitwerk inflects filenames to class names:
- =my_user.rb= → =MyUser=
- =blog_post.rb= → =BlogPost=
- =api_client.rb= → =ApiClient="

## Resources for Upgrade

**Official Guides:**
- [[https://guides.rubyonrails.org/upgrading_ruby_on_rails.html][Rails Upgrading Guide]]
- [[https://railsdiff.org][RailsDiff.org]] - Compare Rails versions

**Version-Specific Guides:**
- [[https://guides.rubyonrails.org/7_2_release_notes.html][Rails 7.2 Release Notes]]
- [[https://guides.rubyonrails.org/8_0_release_notes.html][Rails 8.0 Release Notes]]

**Tools:**
```bash
# Check for deprecated code
bundle exec rails app:update  # Updates config files

# Run deprecation checks
bundle exec rake rails:update:configs
```

## Offering Suggestions

When detecting deprecated patterns:

1. **Identify the deprecation** - What's deprecated and since which version
2. **Explain why it changed** - Brief context on the change
3. **Provide modern alternative** - Show exact replacement code
4. **Note breaking changes** - Mention behavior differences if any
5. **Link to resources** - Point to official docs for details

## Upgrade Checklist

When user mentions upgrading Rails:

1. **Current Rails version?** - Check =Gemfile.lock=
2. **Target version?** - What version are they upgrading to?
3. **Incremental path?** - Plan version-by-version upgrade
4. **Dependencies compatible?** - Check gem compatibility
5. **Tests passing?** - Ensure good test coverage before upgrade
6. **Deprecations addressed?** - Fix warnings in current version first
7. **Config updated?** - =config.load_defaults= matches version
8. **Staging tested?** - Test in non-production environment

## When to Activate

This skill should be **suggestive, not aggressive**:

- Point out deprecated patterns when seen
- Offer upgrade guidance when explicitly asked
- Don't nag about minor deprecations
- Focus on critical/breaking changes
- Respect if user is on legacy version intentionally (maintenance mode)

Always respect that upgrades are significant decisions requiring:
- Time and resources
- Testing effort
- Team coordination
- Potential for breaking changes

Provide information, not pressure.