For project overview, tech stack, architecture reference (models, controllers, services, testing), and more, read AGENTS.md.
Full setup (bundle, npm, database create/migrate/seed):
bin/setup
If you just need frontend dependencies:
npm ci
When the user says "AI files", "AI instructions", "tell AI to", or "remember to always", these are the files. If you notice the user repeatedly correcting the same pattern, suggest adding it to the AI files with a concrete proposal.
| File | Purpose |
|---|---|
CLAUDE.md |
Coding rules and conventions (this file) |
AGENTS.md |
Architecture reference + project details |
.github/copilot-instructions.md |
Coding rules for Copilot (duplicated from CLAUDE.md — keep in sync) |
ai/ |
Shell script shortcuts for common dev tasks |
When changing a model or controller, check whether these related files need updates:
| If you change... | Also check... |
|---|---|
| Model | Decorator, policy, factory, model spec |
| Controller | Policy, request spec, routing spec, views |
| View | System spec, Stimulus controller (if interactive) |
| Service | Service spec |
| Decorator | Decorator spec |
| Mailer (add/remove) | Mailer spec, mailer preview (follow existing patterns) |
| Add/remove model, concern, service, or gem | AGENTS.md |
- Use modern Ruby syntax
- Prefer early returns and guard clauses
- Avoid unnecessary and/or complex conditionals
- Prefer constants and scopes over magic strings
- Use safe navigation (
&.) where appropriate - Use
presenceover blank checks - Use
Arel.sqlfor raw SQL in order clauses - Avoid
update_allunless explicitly intended - Prefer service objects under app/services/
- Prefer POROs over concerns when possible
- Use
after_commitinstead ofafter_savefor side effects
This project uses rubocop-rails-omakase. All code MUST follow these rules:
- Always use double quotes for strings:
"foo"not'foo'
- Spaces inside array brackets:
[ a, b, c ]not[a, b, c](empty arrays:[]) - Spaces inside hash braces:
{ a: 1, b: 2 }not{a: 1}(empty hashes:{}) - Spaces inside block braces:
foo { bar }notfoo {bar}(empty blocks:foo { }) - No spaces inside parens:
foo(bar)notfoo( bar ) - No spaces inside reference brackets:
hash[:key]nothash[ :key ] - Space before block braces:
foo { }notfoo{ }
- No trailing commas in arrays, hashes, or method arguments
- 2-space indentation, no tabs
- Consistent indentation at normal level — do NOT indent methods under
private/protected - Align
endwith the variable in assignments:result = if condition value end
- Align
whenwithend, not withcase
- No trailing whitespace on any line
- No trailing blank lines at end of file
- No empty lines inside class, module, method, or block bodies
- Use
%w[]and%i[]with square bracket delimiters (not parens) - Use modern hash syntax:
{ key: value }not{ :key => value } - No redundant returns — omit
returnon last expression - Use
flat_mapinstead of.map { }.flatten - No redundant
.to_sinside string interpolation - Use
Foo.methodnotFoo::methodfor method calls - No parentheses around conditions:
if foonotif (foo) - No semicolons to separate statements
- Use sentence case for UI labels, headings, and display text — not title case
- "Age range" not "Age Range", "Art type" not "Art Type"
- Use
.underscore.humanizeto convert PascalCase model/type names to sentence case (e.g.,"AgeRange".underscore.humanize→"Age range") - Avoid
.titleizefor user-facing labels — it produces title case - Exception: when a category type name prefixes a category name (e.g., "Age Range: 3-5"), use
.titleizefor the prefix
- Closing
>on same line as last attribute — do not put>on its own line - When attributes span multiple lines, keep the closing
>with the last attribute - Example (GOOD):
<div class="relative z-10 w-full bg-white text-gray-800 py-2 px-4" id="dropdown">
- Example (BAD):
<div class="relative z-10 w-full bg-white text-gray-800 py-2 px-4" id="dropdown" >
- ES6+ syntax, ESM imports/exports,
const/let(novar) - Use
constfor fixed values — notSCREAMING_SNAKE_CASEconstants (e.g.,const styleId = "foo"notconst STYLE_ID = "foo") - Strongly prefer Stimulus for JavaScript behavior — do not write raw/inline JS or jQuery
- Always use Tailwind CSS utility classes for styling — do not write custom CSS unless absolutely necessary
- Prefer Font Awesome (free) icons over inline SVGs — use
icon("fa-solid fa-foo")helper. Inline SVGs are acceptable when a specific icon design is preferred. - Prefer Turbo for navigation and form submissions before reaching for Stimulus
- Controller naming:
[name]_controller.js - Keep controllers focused and small
Follow the Stimulus Handbook and reference docs. Key rules:
Targets over querySelector — declare static targets = [...] and use data-[controller]-target attributes in views. Never use this.element.querySelector or document.getElementById to find elements that could be targets. Exception: elements outside the controller's scope (e.g., in a parent view).
Values API for state — use static values = { name: Type } for any state that persists or drives UI. Do not store state in instance variables when a value would work. Use [name]ValueChanged() callbacks for reactive updates instead of manual syncing.
Actions over manual listeners — use data-action attributes instead of addEventListener in connect(). Omit the event when it's the default for the element (click for buttons/links, input for inputs/textareas, submit for forms, change for selects). Use @window or @document suffixes for global events when possible (e.g., resize@window->controller#layout). Use action options like :prevent and :stop instead of calling event.preventDefault() in methods.
Classes API for CSS — use static classes = [...] when CSS classes need to be configurable from HTML. For standard Tailwind utilities used internally (e.g., "hidden"), hardcoding is acceptable.
Outlets for cross-controller communication — use static outlets = [...] to reference other controllers instead of document.getElementById or custom events when the relationship is stable.
Lifecycle discipline — every listener, timer, or observer created in connect() must be cleaned up in disconnect(). Store bound handler references so they can be removed. Use initialize() for one-time setup (e.g., binding functions).
Target lifecycle callbacks — use [name]TargetConnected(element) and [name]TargetDisconnected(element) to respond to dynamically added/removed targets (e.g., cocoon nested fields, Turbo streams).
Visibility — toggle the hidden class via classList.toggle("hidden", condition) instead of setting style.display. Use class="hidden" in HTML for initial hidden state, not style="display:none".
- Name migration files using UTC timestamps (e.g.,
20260228143000), not sequential numbers (e.g.,20260228000007) - Multiple branches adding migrations on the same date will collide if they use sequential numbering
- Migrations must be reversible — always use explicit
up/downmethods instead ofchangewhen the rollback isn't trivially invertible. Guarddownoperations withif_exists: true,column_exists?,index_exists?, andforeign_key_exists?so rollbacks are idempotent and recover from partial failures
- Default branch is
main - Commit messages should explain why, not what
- CI runs via GitHub Actions (
.github/workflows/) - When rebasing onto main, review incoming changes for their intent and flag any oversights — missing tests, incomplete migrations, broken assumptions, or conflicts between the two branches. Check both directions: schema/model changes on either branch that affect views, partials, or layouts on the other (e.g., main redesigned a table's CSS but your branch adds new columns to it, or vice versa)
- Push to a draft PR early — push commits and create a draft PR (
gh pr create --draft) as soon as work begins, rather than keeping changes in a local branch. Push on every commit. - After completing work, mark the PR ready using
gh pr ready - Do not rename branches after creating a PR — deleting the old remote branch auto-closes the PR on GitHub, and the head ref cannot be changed after creation
- Use
docs/pull_request_template.mdfor PR description structure - Use bullet points, not paragraphs, when filling out each section
- Description must explain why the change was made, not just what
- Include screenshots for UI changes
- On every push, update the PR title and content to reflect the current diff — preserve any existing images/screenshots in the description
- On every push, update AI instruction files if the diff adds, removes, or renames anything tracked in AGENTS.md — specifically: Stimulus controllers, services, model/controller concerns, mailers, rake tasks, and directory file counts
- On every push, add PR review comments on notable lines of code — decisions, trade-offs, non-obvious logic, or anything a reviewer should understand. Use
gh apito post line comments on the diff
This project uses mise to manage the Ruby version. Before running any bundle exec or Ruby command, activate mise:
eval "$(command /opt/homebrew/bin/mise activate zsh)"The ai/ scripts include this automatically.
- Bug fixes require a failing test first — before writing any fix code, write a test that reproduces the bug and confirm it fails. Only then write the code to make it pass.
- Follow the red-green-refactor cycle: failing test, minimal fix, then refactor
- Be careful with system/JS tests — avoid patterns that lead to flakiness
See ai/ directory for executable scripts:
| Command | What it does |
|---|---|
ai/test [args] |
Run RSpec |
ai/lint |
Rubocop on all files |
ai/lint --fix |
Auto-fix lint issues |
ai/server |
Start dev services (web + vite) |
ai/console |
Rails console |
ai/routes -g pattern |
Search Rails routes |
ai/db-migrate |
Run database migrations |