12 Rules for Claude.md (Example)

gistfile1.txt

# CLAUDE.md
Trailmark — an iOS app for logging hikes, backed by a Node.js REST API.
- `ios/` — SwiftUI app, iOS 17+, native components only
- `api/` — Node 22, TypeScript, Express, Postgres 16 (Kysely)
- `packages/contracts/` — zod schemas + generated OpenAPI. Single source of truth for both sides. Change the contract first, then the server, then the client.
Nested rules live in `api/CLAUDE.md` and `ios/CLAUDE.md`. Longer workflows (release, migration, security review) are skills in `.claude/skills/` — do not paste them here.
## Ask before you assume
Never guess at intent. If a task leaves anything open — which screen, which endpoint, what happens on failure, whether it needs a migration, whether this is user-facing — stop and ask. One question up front is cheaper than half a day of work in the wrong direction.
- Ask when the request could reasonably mean two different things.
- Ask before changing a public API shape, a DB schema, or anything in `packages/contracts/`.
- Do not invent product decisions, copy, or acceptance criteria.
- Do not widen scope past what was asked. Note the adjacent thing you spotted; don't fix it unprompted.
- If you had to assume something you couldn't resolve, list it explicitly at the top of your summary.
## The loop
Every change runs through this. A task is not done until it is green.
```bash
npm run check # tsc --noEmit, eslint, prettier --check
npm test # vitest, unit + integration
npm run test:api # supertest against a throwaway Postgres
npm run db:migrate # never edit an applied migration, always add a new one
```
```bash
cd ios && xcodegen && xcodebuild test -scheme Trailmark -destination 'platform=iOS Simulator,name=iPhone 17'
cd ios && swiftlint --strict
```
Rules for the loop:
- Write the failing test first. Watch it fail for the right reason, then make it pass.
- Run `npm run check && npm test` after every meaningful edit, not once at the end.
- Never report success on a red loop. Never disable, skip, or `.only` a test to get to green.
- If a test is wrong, say so and explain why before changing it.
- Do not start long-running processes (`npm run dev`, `expo start`) to "verify" — they never exit. Use the commands above.
## Type checking
Both languages are strict. Type errors are not warnings.
`tsconfig.json` — these stay on:
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"verbatimModuleSyntax": true
}
}
```
- No `any`. If a type is genuinely unknown, use `unknown` and narrow it.
- No `as` to escape an error, and no non-null `!`. Fix the type or narrow properly.
- No `@ts-expect-error` without a comment naming the upstream issue.
- All external input (request bodies, query params, API responses) is parsed with a zod schema from `packages/contracts/`. Types are inferred from the schema, never hand-written alongside it.
- Swift: no force unwraps, no `try!`, no `as!`. Use `guard let`, typed `throws`, and `Result` at boundaries.
Respect these from the start. Do not write loose code and correct it after the type check fails.
## Naming
Consistency is for the model as much as for us. Pick the existing word, don't coin a new one.
**Domain vocabulary — one word per concept:**
| Concept | Use | Never |
| --------------- | ---------------------- | ----------------------------- |
| A recorded walk | `hike` | trip, walk, activity, session |
| The GPS line | `track` | route, path, trace |
| A saved place | `waypoint` | pin, marker, poi |
| Account access | `Sign in` / `Sign out` | Login, Log In, Log out |
**Code:**
- Functions: `createHike`, `getHike`, `listHikes`, `updateHike`, `deleteHike`. Not `fetch`, `remove`, `save`, `handle`.
- Booleans read as assertions: `isSyncing`, `hasTrack`, `canEdit`.
- Routes: kebab-case, plural nouns — `GET /v1/hikes/:hikeId/waypoints`.
- Postgres: snake_case tables and columns, plural tables — `hikes`, `waypoints`, `started_at`.
- Swift types are UpperCamelCase and the file is named after the type: `HikeDetailView.swift`.
- SwiftUI views end in `View`, observable state ends in `Store`: `HikeListView`, `HikeStore`.
- Test files sit beside the source: `hikes.service.ts` → `hikes.service.test.ts`.
**User-facing copy:** sentence case for buttons and labels ("Save hike", not "Save Hike"). Copy strings live in `ios/Trailmark/Resources/Localizable.strings` — no string literals in views.
## Project structure
```
api/
src/
modules// # route.ts, service.ts, repo.ts, *.test.ts
db/ # kysely client, generated types
middleware/
lib/ # shared, dependency-free helpers
migrations/ # timestamped .sql, append-only
ios/
Trailmark/
Features// # View, Store, and their tests
DesignSystem/ # Tokens.swift, reusable components
Networking/ # APIClient, generated contract types
Persistence/ # SwiftData models
packages/
contracts/ # zod schemas, OpenAPI output
```
- New backend work goes in a module folder. Routes never talk to the DB directly — route → service → repo.
- New screens go in `Features/`. Views hold no networking and no business logic; that belongs in the Store.
- Nothing new at the repo root without asking.
## Dependencies
Code is cheap; maintenance isn't. Prefer a well-established package over rolling your own, and prefer the platform over a package.
Before installing anything, check and state:
- Weekly downloads over ~100k, a release in the last six months, more than one maintainer.
- Nothing single-maintainer or freshly published for anything touching auth, crypto, networking, or file I/O.
- No new dependency for something the standard library, SwiftUI, or an existing dependency already does.
Ask before adding a dependency. Never add one as a side effect of another task, and never pin to `latest` — exact versions only, lockfile committed.
Native SwiftUI components only on iOS. No third-party UI frameworks.
## Performance
Every endpoint has a p95 budget of 200ms. Anything slower is a bug, not a tuning opportunity for later.
- Filter, sort, aggregate, and paginate in SQL. Never pull rows into Node to filter them there.
- Every list endpoint is paginated: `limit` defaults to 25, hard maximum 100. No unbounded `SELECT`.
- Select the columns you need. `select *` is not acceptable in a repo function.
- No N+1. Join or batch — one query per request path, not one per row.
- New query patterns come with an index in the same migration. Run `EXPLAIN ANALYZE` and include the plan in your summary if a query touches more than 10k rows.
- Do a performance pass at the end of any feature that reads data, and say what you checked.
On iOS: no work on the main actor beyond UI updates. Lists over 50 items use `LazyVStack`. Images are downsampled before display.
## Error handling
Fail early, fail loudly, and never swallow.
- No empty `catch`. No `catch { console.log(e) }`. Either handle it meaningfully or let it propagate.
- Throw a typed `AppError` with a stable machine-readable code. Unexpected errors surface as 500 and page us — they do not get mapped to a friendly 200.
- Every error response uses this envelope:
```ts
{ error: { code: "hike_not_found", message: "That hike no longer exists.", details?: unknown } }
```
- Messages are useful to the client: what failed, and what to do about it. "Something went wrong" is not an error message.
- Validation failures return 422 with the zod issue list in `details`.
- iOS maps every error code to real copy and a real recovery action. No silent failures, no generic alert as a catch-all.
- Log with structured context (`hikeId`, `userId`, `requestId`), never a bare string.
## End-to-end testing
After any feature that spans both sides, exercise it like a person would — not just with unit tests.
- Boot the API and the simulator, sign in, and drive the actual flow end to end: start a hike, record a track, background the app, foreground it, sync, confirm the row in Postgres.
- Test the unhappy paths deliberately: airplane mode mid-sync, expired token, force-quit during a recording, duplicate submit.
- Report what you clicked and what you saw. If a step failed, keep the failure — don't work around it and call it passing.
You don't need to write Playwright suites for this. Driving the app as a human is the point.
## UI testing
A green test suite tells you nothing about whether the screen looks right.
- Screenshot every screen you touch, at least once, and look at it before you say it's done.
- Check the small iPhone and the largest Dynamic Type setting. Check dark mode.
- Use realistic seed data from `api/seeds/realistic.sql` — real place names, long names, empty states, a 400-item list. Never `Lorem ipsum`, never "Test User".
- Look specifically for: clipped or truncated text, overlapping views, content under the safe area, missing empty state, missing loading state.
## Architecture
Read this before exploring the codebase. If you find yourself searching for something that belongs here, add it (see below).
Request path: SwiftUI view → Store → `APIClient` → Express route → service → repo → Postgres. Auth is a short-lived JWT in the Keychain with a refresh token; `middleware/auth.ts` populates `req.user` and every route below `/v1` requires it.
Sync is offline-first. The app writes to SwiftData immediately, queues a mutation, and reconciles on reconnect. Server timestamps win on conflict; the client never invents an `id` — it sends a client-generated `idempotencyKey`.
**Where things live:**
| You need | Look in |
| -------------------------------- | ------------------------------------------- |
| Auth, tokens, refresh | `api/src/modules/auth/` |
| Request/response schemas | `packages/contracts/src/` |
| DB client and generated types | `api/src/db/` |
| Migrations | `api/migrations/` |
| Colours, spacing, type scale | `ios/Trailmark/DesignSystem/Tokens.swift` |
| Networking, retry, auth headers | `ios/Trailmark/Networking/APIClient.swift` |
| Offline queue and reconciliation | `ios/Trailmark/Persistence/SyncQueue.swift` |
| Env vars and their defaults | `api/src/config.ts` |
| CI pipeline | `.github/workflows/ci.yml` |
## Keeping this file current
This file is a failure log, not a wishlist. Every line below exists because it went wrong at least once.
When you make a mistake, get corrected, or discover something about this codebase that wasn't written down:
1. Add one line to the failure log below, in the imperative, describing the correct behaviour.
2. Keep it specific to this repo. General advice belongs nowhere.
3. If the fix is a workflow rather than a rule, put it in `.claude/skills/` and link it from here.
4. Include the change in the same commit and mention it in your summary.
Keep this file under 500 lines. It is loaded into every session, and long context makes you less reliable, not more. If a section outgrows its usefulness, move it to `api/CLAUDE.md`, `ios/CLAUDE.md`, or a skill.
## Failure log
- Do not run `npm run dev` to verify a change; it never exits. Use `npm run check && npm test`.
- Run migrations against the test database before `npm run test:api`, or every test fails on a missing column.
- `xcodebuild` needs `xcodegen` run first — the `.xcodeproj` is generated and gitignored.
- Do not edit `packages/contracts/dist/`; it is generated by `npm run contracts:build`.
- Do not add `@MainActor` to `HikeStore` methods that already run on the main actor — SwiftLint flags it and it hides real concurrency bugs.
- Waypoint ordering comes from `sequence`, not `created_at`. Offline waypoints arrive out of order.
- Never call `deleteHike` without checking for queued mutations against that hike first.
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论