|
1 | 1 | # Coding Style |
2 | 2 |
|
3 | | -## Plan Time |
4 | | - |
5 | | -### Pre-Implementation Layer Check |
6 | | - |
7 | | -Before writing new logic, decide which layer it belongs to. Run this check at plan time (design/architecture phase, before writing any code): |
8 | | - |
9 | | -| Layer | Directory | Key constraints | |
10 | | -| -------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | |
11 | | -| DB schema | `prisma/` | Migrations are immutable after apply | |
12 | | -| DB access | `src/lib/server/` | Server-only; never import in client code | |
13 | | -| Validation | `src/**/zod/` | `z.number().int()` for Int fields; comment dual-enforcement with SQL CHECK | |
14 | | -| Domain types | `src/**/types/` (`_types/` inside `src/routes/`) | Plural aliases; TSDoc on every export; avoid `any`; see alternatives | |
15 | | -| Test data | `src/**/fixtures/` (`_fixtures/` inside `src/routes/`) | Write before implementation (TDD); use realistic values | |
16 | | -| Business logic | `src/**/services/` | Return pure values or `null`; no `Response`/`json()` | |
17 | | -| Pure utilities | `src/**/utils/` (`_utils/` inside `src/routes/`) | No side effects; adjacent unit test required | |
18 | | -| State | `src/**/stores/` | `.svelte.ts`; class + `$state()`; singleton export | |
19 | | -| Route handlers | `src/routes/` | Page: `redirect()`; API: `error()` | |
20 | | -| UI components | `src/**/*.svelte` | Svelte 5 Runes; business logic → `utils/` | |
21 | | - |
22 | | -## Code Structure |
23 | | - |
24 | | -### Naming |
25 | | - |
26 | | -- **Abbreviations**: avoid non-standard abbreviations (`res` → `response`, `btn` → `button`). When in doubt, spell it out. |
27 | | -- **Lambda parameters**: no single-character names (e.g., use `placement`, `workbook`). Iterator index `i` is the only exception. |
28 | | -- **`upsert`**: only use when the implementation performs both insert and update. For insert-only, use `initialize`, `seed`, or another accurate verb. |
29 | | -- **Function verbs**: every function name must start with a verb. Noun-only names (`pointOnCircle`, `arcPath`) are ambiguous — use `calcPointOnCircle`, `buildArcPath`, etc. Common prefixes: `get` (read existing), `build`/`create` (construct new), `calc`/`compute` (derive by formula), `update`, `fetch`, `resolve`. |
30 | | -- **`any`**: Before using `any`, check the value's origin. If unavoidable: use `ReturnType<typeof fn>` for complex returns, `Partial<T>` for partial objects, `obj as T & { prop: U }` for property extension. Last resort: `as unknown as T`. |
31 | | - |
32 | | -- **UI labels**: if a label does not match actual behavior, update it or add an inline comment explaining the intentional mismatch. |
33 | | -- **Constant names**: reflect what the value IS (content), not what it is used for (purpose). e.g., a set holding all enum tab values is `EXISTING_TABS`, not `VALID_TABS`. |
34 | | -- **New files**: before naming a new file or directory, grep the relevant `src/` directory to confirm existing conventions. Confirm at plan time, not during implementation: |
35 | | - - Custom files in routes (utilities, helpers, etc.): `snake_case` (e.g., `user_profile.ts`) |
36 | | - - SvelteKit special files: follow framework conventions (`+page.svelte`, `+page.server.ts`, `+server.ts`) |
37 | | - - Helper directories inside `src/routes/`: underscore-prefixed (`_utils/`, `_types/`, `_fixtures/`, `_components/`) |
38 | | - - Other directories: `kebab-case` (e.g., `contest-table/`) |
39 | | - |
40 | | -### Syntax |
41 | | - |
42 | | -- **Braces**: always use braces for single-statement `if` blocks. Never `if () return;` — write `if () { return; }`. |
43 | | -- **Domain types over `string`**: when the Prisma schema uses an enum (e.g. `grade: TaskGrade`), the corresponding app-layer type must use the same enum — not `string`. A loose `string` type hides misspellings in fixtures and forces `as TaskGrade` casts throughout the codebase. When a field comes from an external source (form data, query params), validate and narrow it at the boundary; inside the app it must always be the domain type. |
44 | | -- **Plural type aliases**: define `type Placements = Placement[]` instead of using `Placement[]` directly in signatures and variables. |
45 | | -- **Empty `catch` blocks**: never use `catch { }` to silence errors. Either re-throw, log, or add a comment justifying suppression. Silent swallowing hides bugs. |
| 3 | +## Pre-Implementation Layer Check |
46 | 4 |
|
47 | | -```typescript |
48 | | -try { ... } catch (error) { console.error('...'); throw error; } // good |
49 | | -try { localStorage.setItem(key, value); } catch { /* unavailable */ } // good + comment |
50 | | -``` |
51 | | - |
52 | | -### No Hard-Coded Values |
53 | | - |
54 | | -Extract magic numbers and strings to named constants. Never embed literal values whose meaning is not self-evident from the type or immediate context. |
55 | | - |
56 | | -```typescript |
57 | | -// Bad: magic literals embedded inline |
58 | | -if (grade >= 11) { ... } |
59 | | - |
60 | | -const response = await fetch('/api/workbooks/submit', options); |
61 | | - |
62 | | -// Good |
63 | | -const MIN_GRADE = 11; |
64 | | -const SUBMIT_URL = '/api/workbooks/submit'; |
65 | | - |
66 | | -if (grade >= MIN_GRADE) { ... } |
67 | | - |
68 | | -const response = await fetch(SUBMIT_URL, options); |
69 | | -``` |
70 | | - |
71 | | -Place constants at the top of the file, or in a dedicated `constants/` module when shared across files. |
72 | | - |
73 | | -### Function Ordering |
74 | | - |
75 | | -Within a file, order declarations as follows: |
76 | | - |
77 | | -1. Exported functions and classes (public API first) |
78 | | -2. Internal helper functions (supporting the exports above) |
| 5 | +Before writing logic, decide which layer it belongs to: |
79 | 6 |
|
80 | | -Place a private helper immediately after the single export that uses it. Place helpers shared by two or more exports at the end of the file. |
| 7 | +| Layer | Directory | Constraints | |
| 8 | +| -------------- | ------------------------------------------- | ------------------------------------------------------- | |
| 9 | +| DB schema | `prisma/` | Migrations immutable after apply | |
| 10 | +| DB access | `src/lib/server/` | Server-only; never in client | |
| 11 | +| Validation | `src/**/zod/` | `z.number().int()` for Int; dual-enforce with SQL CHECK | |
| 12 | +| Domain types | `src/**/types/` | Plural aliases; TSDoc; avoid `any` | |
| 13 | +| Test data | `src/**/fixtures/` | Write before impl (TDD); realistic values | |
| 14 | +| Business logic | `src/**/services/` | Pure values/`null`; no `Response`/`json()` | |
| 15 | +| External APIs | `src/lib/clients/` or `*/internal_clients/` | Shared → `lib`; feature-scoped → `internal_clients` | |
| 16 | +| Utilities | `src/**/utils/` | No side effects; adjacent unit test required | |
| 17 | +| State | `src/**/stores/` | `.svelte.ts`; class + `$state()`; singleton export | |
| 18 | +| Route handlers | `src/routes/` | Page: `redirect()`; API: `error()` | |
| 19 | +| Components | `src/**/*.svelte` | Svelte 5 Runes; logic → `utils/` | |
81 | 20 |
|
82 | | -### URL Parameter Patterns |
| 21 | +## Naming |
83 | 22 |
|
84 | | -#### null-as-ALL: Omitting Params for "All" State |
| 23 | +- **No abbreviations** unless standard (e.g., `id`, `url`) |
| 24 | +- **Lambda params**: no single-char (except `i` for loops) |
| 25 | +- **Function verbs**: `get` (read), `build`/`create` (construct), `calc`/`compute` (derive), `update`, `fetch`, `resolve` |
| 26 | +- **Domain enums**: use domain type, not `string` (validates at boundary, stays typed inside) |
| 27 | +- **Constants**: reflect content not purpose; extract magic numbers/strings |
| 28 | +- **Files**: `snake_case` in routes, `kebab-case` dirs, underscore-prefix helpers (`_utils/`, `_types/`) |
85 | 29 |
|
86 | | -When a filter has an "all" or "unfiltered" state, omit the parameter entirely rather than using a magic value (e.g., "ALL", "\*"). |
| 30 | +## Syntax |
87 | 31 |
|
88 | | -**Pattern:** |
| 32 | +- **Braces**: always for single-statement `if` blocks |
| 33 | +- **Catch blocks**: never silent; re-throw, log, or comment |
| 34 | +- **Plural aliases**: `type Items = Item[]` in signatures |
| 35 | +- **TSDoc**: every export; `@param`/`@returns` when non-obvious |
89 | 36 |
|
90 | | -- Parse function defaults to `null` when param is absent |
91 | | -- `null` → "show all" (no filter applied) |
92 | | -- URL: `/workbooks?tab=solution` (no `categories=`) |
93 | | -- Browser back button naturally restores default "all" view |
| 37 | +## Type Guards at API Boundaries |
94 | 38 |
|
95 | | -**Benefit:** Cleaner URLs, intuitive history behavior, smaller sessionStorage footprint. |
96 | | - |
97 | | -**Example:** `parseWorkBookCategory()` defaults to `null` = all categories |
98 | | - |
99 | | -### Type Guards: Precise Narrowing for Excluded Values |
100 | | - |
101 | | -When a type guard intentionally excludes enum members, use `Exclude<T, 'VALUE'>` in the return type to match runtime behavior. Caller code then trusts the type system; no `as` casts needed. |
102 | | - |
103 | | -### Layer-Specific Responsibility: Normalization vs Filtering |
104 | | - |
105 | | -When filtering data by multiple criteria (format + role): |
106 | | - |
107 | | -- **Service layer**: Normalize raw data format (e.g., `null → PENDING`). Return all data; stay framework-agnostic and testable. |
108 | | -- **UI layer**: Filter by role/context (e.g., "admin sees PENDING, user doesn't"). Apply display rules in component. |
109 | | - |
110 | | -**Benefit**: Services remain pure and unit-testable without mocking roles; UI logic explicit in one place. |
111 | | - |
112 | | -### Function Composition: Single Responsibility |
113 | | - |
114 | | -Separate independent transformations into distinct functions. Compose at call site. |
| 39 | +When receiving enum values from external APIs, validate with a type guard: |
115 | 40 |
|
116 | 41 | ```typescript |
117 | | -// groupBySolutionCategory(): pure grouping (testable) |
118 | | -// filterGroupsByRole(): pure filtering by role (testable) |
119 | | -let filtered = $derived(filterGroupsByRole(groupBySolutionCategory(workbooks, map), role)); |
| 42 | +// Good: `isValidTaskGrade(value): value is TaskGrade` |
| 43 | +const grade = isValidTaskGrade(data.grade) ? data.grade : null; |
120 | 44 | ``` |
121 | 45 |
|
122 | | -**Benefit**: Each function testable in isolation; reusable in different contexts. |
123 | | - |
124 | | -## Documentation |
125 | | - |
126 | | -### Language Policy |
| 46 | +Extract to `src/lib/utils/` with adjacent tests. |
127 | 47 |
|
128 | | -Write all project documentation (plans, dev-notes, guides, refactor notes) in Japanese. Write all source code comments, TSDoc, commit messages, and test titles in English. This keeps documentation readable for the team while keeping code comments universally accessible and searchable. |
| 48 | +## Dead Code: Three-Condition Rule |
129 | 49 |
|
130 | | -**Exception**: The `## CodeRabbit Findings` section in `plan.md` must quote findings verbatim in their original language (English). Do not translate CodeRabbit output. |
| 50 | +Delete function only if: (1) zero callers, (2) replacement exists, (3) dependent fields also deleted. |
131 | 51 |
|
132 | | -### TSDoc |
133 | | - |
134 | | -Add TSDoc to every exported function, type, class. Minimum: `@param` (non-obvious), `@returns` (not evident from type). One-liner OK; multi-line for complex behavior only. |
135 | | - |
136 | | -### Documentation |
| 52 | +## Documentation |
137 | 53 |
|
138 | | -Write plans/dev-notes/guides in Japanese. Source code comments in English. Always specify language on code blocks (e.g., `typescript`, `sql`, `bash`). For Svelte 5 unclear behavior: fetch official docs via WebFetch, not training knowledge. |
| 54 | +- **Plans/dev-notes**: Japanese |
| 55 | +- **Code/commits/tests**: English |
| 56 | +- **TSDoc**: required on all exports |
| 57 | +- **Code blocks**: specify language (`typescript`, `sql`, `bash`) |
139 | 58 |
|
140 | | -## Security & Code Review |
| 59 | +## Security |
141 | 60 |
|
142 | | -- **Logging**: No user-identifiable data in `console.log`. Single authoritative log in service layer. |
143 | | -- **CodeRabbit**: Run after all phases complete. Write `critical`/`high`/`medium` findings to `plan.md` verbatim; defer `nitpick` to PR CI. |
| 61 | +- No user-identifiable data in logs |
| 62 | +- No Prisma imports in route handlers |
| 63 | +- Validate input at system boundaries |
| 64 | +- Return safe defaults on service errors |
0 commit comments