|
2 | 2 |
|
3 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
4 | 4 |
|
| 5 | +**Top-level rules — these override defaults:** |
| 6 | + |
| 7 | +- **Zero comments** unless explaining a non-obvious *why*. See [Code conventions](#code-conventions). |
| 8 | +- **Simple, readable code over clever code.** Prefer 3 obvious lines over 1 dense line. |
| 9 | +- **Atomic commits** after each logical unit of work. See [Commits](#commits--atomic-always). |
| 10 | +- **Before creating** a new hook, util, or component — grep for an existing one. Follow existing patterns. |
| 11 | + |
5 | 12 | ## Project Overview |
6 | 13 |
|
7 | 14 | Thinkix is an AI-native infinite canvas whiteboard built with Next.js 16, React 19, and the Plait board library. The AI agent can read the current board, create new structures, and edit existing elements via a Unix-shell-style command set and a custom DSL. The project uses Bun workspaces for shared code and Liveblocks + Yjs for live collaboration. |
@@ -195,8 +202,91 @@ NEXT_PUBLIC_POSTHOG_HOST=... |
195 | 202 |
|
196 | 203 | ## Code conventions |
197 | 204 |
|
198 | | -- Self-documenting code; no ceremonial comments. Only comment non-obvious *why*. |
| 205 | +### Comments — default is zero |
| 206 | + |
| 207 | +Write code that does not need comments. Before adding any comment, apply this test: **does it explain WHY something is done this way, in a way the code itself cannot?** If it describes WHAT the code does, delete it and rename variables/functions until the code speaks for itself. |
| 208 | + |
| 209 | +**Never write comments like these:** |
| 210 | + |
| 211 | +```ts |
| 212 | +// Loop through elements and filter selected ones |
| 213 | +const selected = elements.filter(isSelected); |
| 214 | + |
| 215 | +// Set the active tool to pen |
| 216 | +setActiveTool('pen'); |
| 217 | + |
| 218 | +// Check if the board exists |
| 219 | +if (!board) return null; |
| 220 | + |
| 221 | +/** Handles the click event */ |
| 222 | +function handleClick() { ... } |
| 223 | + |
| 224 | +// Initialize state |
| 225 | +const [count, setCount] = useState(0); |
| 226 | +``` |
| 227 | + |
| 228 | +**These are acceptable** (non-obvious why, workaround context, gotcha warnings): |
| 229 | + |
| 230 | +```ts |
| 231 | +// Plait re-orders jsonb keys on write; serialize canonically or prompt |
| 232 | +// caching breaks downstream. See #142. |
| 233 | +const payload = canonicalize(element); |
| 234 | + |
| 235 | +// updatePointerType has no native hook — patched in with-tool-sync.ts |
| 236 | +BoardTransforms.updatePointerType = patchedUpdatePointerType; |
| 237 | + |
| 238 | +// Liveblocks presence uses screen coords; we convert to canvas/world |
| 239 | +// coords here so cursors render correctly across different viewports. |
| 240 | +const worldPos = screenToCanvas(screenPos, viewport); |
| 241 | +``` |
| 242 | + |
| 243 | +JSDoc is allowed only on exported package APIs (`packages/*`), never on internal functions whose name and signature already tell the story. |
| 244 | + |
| 245 | +### Simplicity over cleverness |
| 246 | + |
| 247 | +Optimize for the next reader, not for elegance or impressiveness. |
| 248 | + |
| 249 | +**Do:** |
| 250 | +```ts |
| 251 | +const activeUsers = users.filter(user => user.isActive); |
| 252 | +const names = activeUsers.map(user => user.name); |
| 253 | +``` |
| 254 | + |
| 255 | +**Don't:** |
| 256 | +```ts |
| 257 | +const names = users.reduce((acc, u) => (u.isActive ? [...acc, u.name] : acc), [] as string[]); |
| 258 | +``` |
| 259 | + |
| 260 | +Rules: |
| 261 | +- Prefer a plain `for`/`filter`/`map` chain over a dense one-liner with nested ternaries, `reduce`-to-build-objects, or point-free composition. |
| 262 | +- Prefer 3 obvious lines over 1 dense line. Always. |
| 263 | +- Prefer duplication over the wrong abstraction — do not invent a helper or generic until the third usage. |
| 264 | +- Early returns over nested conditionals: `if (!board) return null` at the top, not an `else` branch at the bottom. |
| 265 | +- No nested ternaries. Extract to a variable or use `if`/`else`. |
| 266 | +- If you feel the urge to add a comment explaining clever logic — that is the signal to rewrite the logic, not add the comment. |
| 267 | + |
| 268 | +### Naming |
| 269 | + |
| 270 | +- Functions: verbs (`createBoard`, `handleClick`, `resolveProvider`). |
| 271 | +- Variables/constants: nouns (`activeUsers`, `boardId`, `GRID_SIZE`). |
| 272 | +- Booleans: adjectives/state (`isActive`, `hasPermission`, `shouldRender`). |
| 273 | +- Names should be specific enough to be grep-able. Avoid single letters except in tight lambda args (`items.map(x => x.id)` is fine; `function f(x, y, z)` is not). |
| 274 | + |
| 275 | +### Commits — atomic, always |
| 276 | + |
| 277 | +Commit after each logical unit of work. Do not batch unrelated changes. |
| 278 | + |
| 279 | +- One logical change per commit. Refactors, behavior changes, and test additions are separate commits even within the same task. |
| 280 | +- If a fix requires a preparatory refactor, commit the refactor first (with no behavior change), then commit the fix. |
| 281 | +- Conventional commit format: `fix(collab): …`, `feat(agent): …`, `refactor(board): …`, `test: …`, `chore: …`. |
| 282 | +- Never mix generated files (`parser.js`) with hand-written changes unless the generation is caused by that change — and say so in the commit body. |
| 283 | +- Commit messages: subject says WHAT changed, body (when needed) says WHY. |
| 284 | + |
| 285 | +### General |
| 286 | + |
199 | 287 | - Feature modules expose a small `index.ts` barrel. Follow existing patterns instead of introducing new ones. |
| 288 | +- Before creating a new hook, util, or component, grep the codebase for an existing one that does the same thing. |
200 | 289 | - Types flow from `@thinkix/shared` (no JSX) and `shared/constants` (JSX allowed). |
201 | | -- Prefer early returns (`if (!board) return null`). |
202 | | -- Keep PRs focused; if you change the DSL grammar, regenerate the parser before opening the PR (see `CONTRIBUTING.md`). |
| 290 | +- `const`-first. Only use `let` when reassignment is genuinely needed. |
| 291 | +- Single-responsibility functions. If a function does two things, split it. |
| 292 | +- Keep PRs focused; if you change the DSL grammar, regenerate the parser before opening the PR (see `CONTRIBUTING.md`). |
0 commit comments