Skip to content
61 changes: 61 additions & 0 deletions rails/ai-rules/project-memory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# thoughtbot project architecture and coding standards for Rails development using agents

See the folder `rules` for language-specific guidelines, testing conventions,
and other standards.

> **Usage:**
>
> 1. Copy the content of this file.
> 2. Create a new file in the root of your project called `.claude/CLAUDE.md`.
> 3. Update the information in the new file to match your project.
> 4. Paste the content of this file into the new file.
> 5. Copy the rules folder into `.claude/`

## Project: [APP_NAME]

[One sentence: what the app does and who it serves.]

## Stack

thoughtbot opinionated defaults based on Suspenders:

- Ruby [VERSION] / Rails [VERSION]
- PostgreSQL, Redis
- Sidekiq for background jobs
- Puma web server
- RSpec, FactoryBot, Shoulda Matchers, Capybara, WebMock
- Standard Ruby for linting (not RuboCop)
- SimpleCov for test coverage
- inline_svg for SVG embedding
- bundler-audit for dependency security
- GitHub Actions CI
- Heroku deployment
- Authentication: Devise
- [Authorization: Pundit / custom / etc.]

## Commands

```bash
bin/rails server # Start dev server
bin/rails spec # Full test suite (Suspenders rake task)
bundle exec rspec spec/models # Model specs only
bundle exec rspec spec/requests # Request specs only
rake standard # Lint
rake standard:fix # Auto-fix lint issues
bin/rails db:migrate # Run migrations
bin/rails suspenders:db:migrate # Migrate + annotate
bin/rails suspenders:cleanup:organize_gemfile # Sort Gemfile
bundle audit # Check gem vulnerabilities
bin/rails routes # View routes
```

## Rules

Short constraints in .claude/rules/:

rules/models.md — models conventions
rules/controllers.md — controller conventions
rules/testing.md - **MUST write tests first** TDD guidelines
rules/security.md — security guidelines
rules/views.md — No logic in views, presenter usage, Turbo conventions
rules/database.md — Indexes, N+1, migration rules, query guidelines
10 changes: 10 additions & 0 deletions rails/ai-rules/rules/controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Controllers

- Controllers handle HTTP only: receive request, delegate to model, return response.
- Actions must not exceed 10 lines (excluding strong params).
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Maximum one instance variable per action.
- No business logic, calculations, email sending, or multi-object operations in controllers.
- Always use strong parameters. Never `params.permit!`.
- Return `status: :unprocessable_entity` on failed form renders (required by Turbo).
- Prefer RESTful routes. Custom actions often mean a missing resource — e.g., `POST /order_cancellations` not `POST /orders/cancel`.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Use `before_action` for auth. Be explicit with `only:` / `except:`.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
10 changes: 10 additions & 0 deletions rails/ai-rules/rules/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Database & Migrations
Comment thread
laicuRoot marked this conversation as resolved.

- Every foreign key must have an index. Every `where` column should have an index.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Always eager-load associations when iterating: `includes`, `preload`, or `eager_load`.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Migrations must be reversible. Never modify data in structure migrations.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Add `null: false` and database-level defaults where appropriate.
- Use `text` over `string` if length varies significantly.
- Wrap multi-record operations in transactions. Use `save!` (bang) inside transactions.
- Keep scopes as one-liners. Complex queries belong in search/query objects.
- Never use `Post.all` without pagination. Avoid `.count` in loops — use `counter_cache`.
10 changes: 10 additions & 0 deletions rails/ai-rules/rules/models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Models & Domain Objects

- No service objects. All domain classes live in `app/models/` with namespaces, never `app/services/`.
- Name classes after domain nouns, not actions. No `*Service`, `*Manager`, `*Handler` suffixes.
- Use `ActiveModel::Model` for POROs that need validation or form integration.
- Replace `.call` / `.perform` with domain verbs: `#save`, `#complete`, `#submit`, `#deliver`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just service objects by another name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it can be, if you're not thoughtful about it. Perhaps these rules oversimplify that what really matters is whether the class represents a genuine concept in the domain. That said, they've been useful for me when I'm working with Claude. With these rules Claude stop suggesting yet another service object when there are already plenty in the codebase.

- Extract from ActiveRecord models when they exceed: 200 lines, 15 public methods, or 7 private methods.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Callbacks only for data integrity (normalise fields, set defaults). Never for emails, payments, or external systems.
- Prefer composition over inheritance. Extract behaviour into small, focused objects.
- Avoid feature envy, long parameter lists (max 3 args), case statements on type, and mixin abuse.
13 changes: 13 additions & 0 deletions rails/ai-rules/rules/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Security

- Never interpolate user input into SQL. Use parameterised queries or `where(key: value)`.
- Always use strong parameters. Never `params.permit!`.
- Scope all queries to the current user or use Pundit authorisation.
- Every controller must have authentication unless explicitly public.
- Never use `raw`, `html_safe`, or `<%==` with user-supplied data.
- Never skip CSRF verification for browser-facing controllers.
- Filter sensitive params in logs: passwords, tokens, secrets, API keys.
- Never `render json: model` without explicit `only:` — whitelist attributes.
- Never redirect to `params[:return_to]` without validation.
- Use array form for system commands: `system("cmd", arg)`, never `system("cmd #{arg}")`.
- Run `bundle audit` regularly. Keep gems updated for security patches.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
13 changes: 13 additions & 0 deletions rails/ai-rules/rules/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Testing
Comment thread
laicuRoot marked this conversation as resolved.

- You MUST not use `let` or `before` in specs.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated
- Test behaviour, not implementation. Four Phase Test: setup, exercise, verify, teardown.
- Test pyramid: many model/PORO unit specs, some request specs, few system specs.
- Every public method on every model and PORO must have at least one spec.
Comment thread
laicuRoot marked this conversation as resolved.
- Use `build` / `build_stubbed` over `create` unless persistence is needed.
- Factories: only required attributes with sensible defaults. Start in `spec/factories.rb`.
- Shoulda Matchers for validations and associations.
- WebMock blocks all external HTTP in tests — always stub external requests.
- One `expect` per `it` block. Max 2 levels of context nesting.
- Never test private methods directly. Never stub the system under test.
- Coverage targets: Models 90%, POROs 95%, Controllers 80%, Helpers/Mailers 100%.
8 changes: 8 additions & 0 deletions rails/ai-rules/rules/views.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Views & Presenters

- Views render data. No calculations, queries, or complex conditionals.
- Use presenters to display logic. Instantiate in controller, use in view.
- Extract repeated markup into partials. Pass data via `locals:`, not instance variables.
- Helpers for simple formatting only (dates, currencies). If longer than 5 lines, use a presenter.
- Turbo: return `status: :unprocessable_entity` on failed forms. Keep Stimulus controllers small.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably a Hotwire / Stimulus rule file to come in the future :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rubocop's Rails/HttpStatusNameConsistency recommends usage of :unprocessable_content:

Suggested change
- Turbo: return `status: :unprocessable_entity` on failed forms. Keep Stimulus controllers small.
- Turbo: return `status: :unprocessable_content` on failed forms. Keep Stimulus controllers small.

- Suspenders provides `_flashes.html.erb` and `_form_errors.html.erb` partials — use them.
Comment thread
laicuRoot marked this conversation as resolved.
Outdated