|
1 | | -# This project is a Ruby on Rails application. |
| 1 | +# This is a Ruby on Rails application. |
| 2 | +<!-- Keep code style rules in sync with CLAUDE.md --> |
| 3 | + |
| 4 | +For project overview, tech stack, architecture reference (models, controllers, services, testing), and more, read `AGENTS.md`. |
| 5 | + |
| 6 | +## Setup |
| 7 | + |
| 8 | +Full setup (bundle, npm, database create/migrate/seed): |
| 9 | +``` |
| 10 | +bin/setup |
| 11 | +``` |
| 12 | + |
| 13 | +If you just need frontend dependencies: |
| 14 | +``` |
| 15 | +npm ci |
| 16 | +``` |
| 17 | + |
| 18 | +## AI Instruction Files |
| 19 | + |
| 20 | +When the user says "AI files", "AI instructions", "tell AI to", or "remember to always", these are the files. |
| 21 | +If you notice the user repeatedly correcting the same pattern, suggest adding it to the AI files with a concrete proposal. |
| 22 | + |
| 23 | +| File | Purpose | |
| 24 | +|---|---| |
| 25 | +| `CLAUDE.md` | Coding rules and conventions (this file) | |
| 26 | +| `AGENTS.md` | Architecture reference + project details | |
| 27 | +| `.github/copilot-instructions.md` | Coding rules for Copilot (duplicated from CLAUDE.md — keep in sync) | |
| 28 | +| `ai/` | Shell script shortcuts for common dev tasks | |
| 29 | + |
| 30 | +## Related Files |
| 31 | + |
| 32 | +When changing a model or controller, check whether these related files need updates: |
| 33 | + |
| 34 | +| If you change... | Also check... | |
| 35 | +|---|---| |
| 36 | +| Model | Decorator, policy, factory, model spec | |
| 37 | +| Controller | Policy, request spec, routing spec, views | |
| 38 | +| View | System spec, Stimulus controller (if interactive) | |
| 39 | +| Service | Service spec | |
| 40 | +| Decorator | Decorator spec | |
| 41 | +| Mailer (add/remove) | Mailer spec, mailer preview (follow existing patterns) | |
| 42 | +| Add/remove model, concern, service, or gem | AGENTS.md | |
| 43 | + |
| 44 | +## Code Style |
2 | 45 |
|
3 | | -# Code style requirements: |
4 | 46 | - Use modern Ruby syntax |
5 | 47 | - Prefer early returns and guard clauses |
6 | 48 | - Avoid unnecessary and/or complex conditionals |
7 | | -- Prefer enums and scopes over magic strings |
| 49 | +- Prefer constants and scopes over magic strings |
8 | 50 | - Use safe navigation (`&.`) where appropriate |
9 | 51 | - Use `presence` over blank checks |
10 | 52 | - Use `Arel.sql` for raw SQL in order clauses |
11 | 53 | - Avoid `update_all` unless explicitly intended |
12 | | -- Prefer service objects under app/services |
| 54 | +- Prefer service objects under app/services/ |
13 | 55 | - Prefer POROs over concerns when possible |
14 | 56 | - Use `after_commit` instead of `after_save` for side effects |
15 | 57 |
|
16 | | -# RuboCop (rubocop-rails-omakase) |
| 58 | +## RuboCop (rubocop-rails-omakase) |
| 59 | + |
17 | 60 | This project uses rubocop-rails-omakase. All code MUST follow these rules: |
18 | 61 |
|
19 | | -## Strings |
20 | | -- Always use double quotes: `"foo"` not `'foo'` |
21 | | - |
22 | | -## Spacing |
23 | | -- Spaces inside array brackets: `[ a, b, c ]` not `[a, b, c]` (empty arrays: `[]`) |
24 | | -- Spaces inside hash braces: `{ a: 1, b: 2 }` not `{a: 1}` (empty hashes: `{}`) |
25 | | -- Spaces inside block braces: `foo { bar }` not `foo {bar}` (empty blocks: `foo { }`) |
26 | | -- No spaces inside parens: `foo(bar)` not `foo( bar )` |
27 | | -- No spaces inside reference brackets: `hash[:key]` not `hash[ :key ]` |
28 | | -- Space before block braces: `foo { }` not `foo{ }` |
29 | | - |
30 | | -## Commas |
31 | | -- No trailing commas in arrays, hashes, or method arguments |
32 | | - |
33 | | -## Indentation |
34 | | -- 2-space indentation, no tabs |
35 | | -- Consistent indentation at normal level — do NOT indent methods under `private`/`protected` |
36 | | -- Align `end` with the variable in assignments |
37 | | -- Align `when` with `end`, not with `case` |
38 | | - |
39 | | -## Whitespace |
40 | | -- No trailing whitespace on any line |
41 | | -- No trailing blank lines at end of file |
42 | | -- No empty lines inside class, module, method, or block bodies |
43 | | - |
44 | | -## Syntax |
45 | | -- Use `%w[]` and `%i[]` with square bracket delimiters (not parens) |
46 | | -- Use modern hash syntax: `{ key: value }` not `{ :key => value }` |
47 | | -- No redundant returns — omit `return` on last expression |
48 | | -- Use `flat_map` instead of `.map { }.flatten` |
49 | | -- No redundant `.to_s` inside string interpolation |
50 | | -- Use `Foo.method` not `Foo::method` for method calls |
51 | | -- No parentheses around conditions: `if foo` not `if (foo)` |
52 | | -- No semicolons to separate statements |
| 62 | +### Strings |
| 63 | +- **Always use double quotes** for strings: `"foo"` not `'foo'` |
| 64 | + |
| 65 | +### Spacing |
| 66 | +- **Spaces inside array brackets:** `[ a, b, c ]` not `[a, b, c]` (empty arrays: `[]`) |
| 67 | +- **Spaces inside hash braces:** `{ a: 1, b: 2 }` not `{a: 1}` (empty hashes: `{}`) |
| 68 | +- **Spaces inside block braces:** `foo { bar }` not `foo {bar}` (empty blocks: `foo { }`) |
| 69 | +- **No spaces inside parens:** `foo(bar)` not `foo( bar )` |
| 70 | +- **No spaces inside reference brackets:** `hash[:key]` not `hash[ :key ]` |
| 71 | +- **Space before block braces:** `foo { }` not `foo{ }` |
| 72 | + |
| 73 | +### Commas |
| 74 | +- **No trailing commas** in arrays, hashes, or method arguments |
| 75 | + |
| 76 | +### Indentation |
| 77 | +- **2-space indentation**, no tabs |
| 78 | +- **Consistent indentation** at normal level — do NOT indent methods under `private`/`protected` |
| 79 | +- **Align `end` with the variable** in assignments: |
| 80 | + ```ruby |
| 81 | + result = if condition |
| 82 | + value |
| 83 | + end |
| 84 | + ``` |
| 85 | +- **Align `when` with `end`**, not with `case` |
| 86 | + |
| 87 | +### Whitespace |
| 88 | +- **No trailing whitespace** on any line |
| 89 | +- **No trailing blank lines** at end of file |
| 90 | +- **No empty lines** inside class, module, method, or block bodies |
| 91 | + |
| 92 | +### Syntax |
| 93 | +- **Use `%w[]` and `%i[]`** with square bracket delimiters (not parens) |
| 94 | +- **Use modern hash syntax:** `{ key: value }` not `{ :key => value }` |
| 95 | +- **No redundant returns** — omit `return` on last expression |
| 96 | +- **Use `flat_map`** instead of `.map { }.flatten` |
| 97 | +- **No redundant `.to_s`** inside string interpolation |
| 98 | +- **Use `Foo.method`** not `Foo::method` for method calls |
| 99 | +- **No parentheses around conditions:** `if foo` not `if (foo)` |
| 100 | +- **No semicolons** to separate statements |
| 101 | + |
| 102 | +## Casing |
| 103 | + |
| 104 | +- **Use sentence case** for UI labels, headings, and display text — not title case |
| 105 | +- "Age range" not "Age Range", "Art type" not "Art Type" |
| 106 | +- Use `.underscore.humanize` to convert PascalCase model/type names to sentence case (e.g., `"AgeRange".underscore.humanize` → `"Age range"`) |
| 107 | +- Avoid `.titleize` for user-facing labels — it produces title case |
| 108 | +- **Exception:** when a category type name prefixes a category name (e.g., "Age Range: 3-5"), use `.titleize` for the prefix |
| 109 | + |
| 110 | +## HTML/ERB Formatting |
| 111 | + |
| 112 | +### Tag Attributes |
| 113 | +- **Closing `>` on same line as last attribute** — do not put `>` on its own line |
| 114 | +- When attributes span multiple lines, keep the closing `>` with the last attribute |
| 115 | +- Example (GOOD): |
| 116 | + ```erb |
| 117 | + <div class="relative z-10 w-full bg-white text-gray-800 py-2 px-4" |
| 118 | + id="dropdown"> |
| 119 | + ``` |
| 120 | +- Example (BAD): |
| 121 | + ```erb |
| 122 | + <div class="relative z-10 w-full bg-white text-gray-800 py-2 px-4" |
| 123 | + id="dropdown" |
| 124 | + > |
| 125 | + ``` |
| 126 | + |
| 127 | +## JavaScript |
| 128 | + |
| 129 | +- ES6+ syntax, ESM imports/exports, `const`/`let` (no `var`) |
| 130 | +- Use `const` for fixed values — not `SCREAMING_SNAKE_CASE` constants (e.g., `const styleId = "foo"` not `const STYLE_ID = "foo"`) |
| 131 | +- **Strongly prefer Stimulus** for JavaScript behavior — do not write raw/inline JS or jQuery |
| 132 | +- **Always use Tailwind CSS** utility classes for styling — do not write custom CSS unless absolutely necessary |
| 133 | +- **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. |
| 134 | +- Prefer Turbo for navigation and form submissions before reaching for Stimulus |
| 135 | +- Controller naming: `[name]_controller.js` |
| 136 | +- Keep controllers focused and small |
| 137 | + |
| 138 | +### Stimulus Conventions |
| 139 | + |
| 140 | +Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction) and reference docs. Key rules: |
| 141 | + |
| 142 | +**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). |
| 143 | + |
| 144 | +**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. |
| 145 | + |
| 146 | +**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. |
| 147 | + |
| 148 | +**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. |
| 149 | + |
| 150 | +**Outlets for cross-controller communication** — use `static outlets = [...]` to reference other controllers instead of `document.getElementById` or custom events when the relationship is stable. |
| 151 | + |
| 152 | +**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). |
| 153 | + |
| 154 | +**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). |
| 155 | + |
| 156 | +**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"`. |
| 157 | + |
| 158 | +## Migrations |
| 159 | + |
| 160 | +- Name migration files using **UTC timestamps** (e.g., `20260228143000`), not sequential numbers (e.g., `20260228000007`) |
| 161 | +- Multiple branches adding migrations on the same date will collide if they use sequential numbering |
| 162 | + |
| 163 | +## Git |
| 164 | + |
| 165 | +- Default branch is `main` |
| 166 | +- Commit messages should explain why, not what |
| 167 | +- CI runs via GitHub Actions (`.github/workflows/`) |
| 168 | +- **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) |
| 169 | + |
| 170 | +## PRs |
| 171 | + |
| 172 | +- **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. |
| 173 | +- After completing work, **mark the PR ready** using `gh pr ready` |
| 174 | +- Once the PR is created, **prepend the PR number to the branch name** (e.g., rename `maebeale/fix-login` to `maebeale/1234-fix-login`) using `git branch -m` and `git push origin -u` with the new name, then delete the old remote branch |
| 175 | +- Use `docs/pull_request_template.md` for PR description structure |
| 176 | +- Use bullet points, not paragraphs, when filling out each section |
| 177 | +- Description must explain why the change was made, not just what |
| 178 | +- Include screenshots for UI changes |
| 179 | +- **On every push**, update the PR title and content to reflect the current diff — preserve any existing images/screenshots in the description |
| 180 | +- **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 |
| 181 | + |
| 182 | +## Quick Commands |
| 183 | + |
| 184 | +See `ai/` directory for executable scripts: |
| 185 | + |
| 186 | +| Command | What it does | |
| 187 | +|---|---| |
| 188 | +| `ai/test [args]` | Run RSpec | |
| 189 | +| `ai/lint` | Rubocop on all files | |
| 190 | +| `ai/lint --fix` | Auto-fix lint issues | |
| 191 | +| `ai/server` | Start dev services (web + vite) | |
| 192 | +| `ai/console` | Rails console | |
| 193 | +| `ai/routes -g pattern` | Search Rails routes | |
| 194 | +| `ai/db-migrate` | Run database migrations | |
0 commit comments