Skip to content

Commit 422d5ef

Browse files
authored
Merge pull request #3281 from AtCoder-NoviSteps/#3280
refactor: Extract workbook list and add tests (#3280)
2 parents e33f95d + 7580ae4 commit 422d5ef

36 files changed

Lines changed: 1644 additions & 434 deletions

.claude/rules/coding-style.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
- **`any`**: before using `any`, check the value's origin — adding a missing `@types/*` or `devDependency` often provides the correct type.
99
- **UI labels**: if a label does not match actual behavior, update it or add an inline comment explaining the intentional mismatch.
1010

11+
## TSDoc
12+
13+
Add TSDoc comments to every exported function, type, and class. The minimum required fields are `@param` (for non-obvious parameters) and `@returns` (when the return value is not evident from the type). One-liner `/** ... */` is sufficient for simple cases; use multi-line only when behaviour needs explanation.
14+
15+
```typescript
16+
/** Returns the URL slug for a workbook, falling back to the workbook ID. */
17+
export function getUrlSlugFrom(workbook: WorkbookList): string { ... }
18+
```
19+
1120
## Syntax
1221

1322
- **Braces**: always use braces for single-statement `if` blocks. Never `if () return;` — write `if () { return; }`.
@@ -43,6 +52,20 @@ the server state is correct but the client-side update is wrong.
4352
**Diagnostic**: "Not reflected live, but fixed after reload" → suspect the optimistic
4453
update payload, not the reactivity system.
4554

55+
## Server-side Logging
56+
57+
Do not log user-identifiable or content data (titles, names, IDs that map to users) in server-side `console.log`. Use generic messages instead:
58+
59+
```typescript
60+
// Bad: leaks content and user identity
61+
console.log(`Created workbook "${workBook.title}" by user ${author.id}`);
62+
63+
// Good
64+
console.log('Workbook created successfully');
65+
```
66+
67+
Prefer placing the single authoritative log in the service layer; remove duplicate logs in route handlers that cover the same event.
68+
4669
## Async Rollback: Capture State Before `await`
4770

4871
Capture `$state` values before the first `await` for safe rollback. A concurrent update can overwrite the variable while awaiting:

.claude/rules/prisma-db.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ paths:
3333

3434
Use `prisma.$transaction()` for multi-step operations.
3535

36+
## Parallel Queries
37+
38+
When `+page.server.ts` `load` makes multiple independent queries, run them concurrently with `Promise.all` to reduce page load latency:
39+
40+
```typescript
41+
// NG: sequential — each awaits the previous
42+
const workbooks = await getWorkBooksWithAuthors();
43+
const tasks = await getTasksByTaskId();
44+
const results = await getTaskResultsOnlyResultExists(userId, true);
45+
46+
// OK: all three fire at once
47+
const [workbooks, tasks, results] = await Promise.all([
48+
getWorkBooksWithAuthors(),
49+
getTasksByTaskId(),
50+
getTaskResultsOnlyResultExists(userId, true),
51+
]);
52+
```
53+
54+
Only applies when queries are **independent** (no output of one used as input to another).
55+
3656
## N+1 Queries
3757

3858
Replace per-item DB calls in loops with a bulk fetch + `Map`:

.claude/rules/svelte-components.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@ Referencing `$props()` inside `$state()` initializer triggers "This reference on
3939
let count = $state(untrack(() => initialCount)); // intentional: prop is initial seed only
4040
```
4141

42+
## `$effect` — Store Reading
43+
44+
Inside `$effect`, use `$store` syntax, not `get(store)`. `get()` bypasses the signal graph — the effect will not re-run when the store updates:
45+
46+
```svelte
47+
// Bad: get() takes a snapshot; effect won't react to store changes
48+
$effect(() => {
49+
const grade = get(myStore).get(key) ?? fallback;
50+
});
51+
52+
// Good: $store subscribes and re-runs the effect on updates
53+
$effect(() => {
54+
const grade = $myStore.get(key) ?? fallback;
55+
});
56+
```
57+
58+
## `$derived` — No Arrow Wrapper
59+
60+
Use `$derived(expr)`, not `$derived(() => expr)`. The arrow form makes the derived value a _function_, not a reactive value — dependencies may not be tracked and the template call site is confusing.
61+
62+
## `{@const}` Placement
63+
64+
`{@const}` must be an **immediate child** of a block statement (`{#if}`, `{#each}`, `{:else}`, `{#snippet}`, etc.). Placing it inside an HTML element is a compile error:
65+
4266
## `{#snippet}` Placement
4367

4468
Define snippets at the **top level**, outside component tags. Inside a tag = named slot = type error:
@@ -57,6 +81,8 @@ Define snippets at the **top level**, outside component tags. Inside a tag = nam
5781
Prefer `{#snippet}` when: (1) needs direct `$state` access, (2) pure display only, (3) same-file DRY.
5882
Promote to component when: independent state/lifecycle needed, exceeds ~30 lines, or reused across files.
5983

84+
**Sibling consistency** — when one sibling block warrants snippet extraction, extract its parallel siblings too, even if they are short. A parent template that mixes inline markup with `{@render}` calls is harder to scan than one where every top-level section is a named snippet.
85+
6086
## Component Boundaries
6187

6288
- One component, one responsibility: don't mix display, state management, and data fetching
@@ -79,7 +105,17 @@ export function buildUpdatedUrl(url: URL, activeTab: ActiveTab): URL { ... }
79105
replaceState(buildUpdatedUrl($page.url, activeTab), {});
80106
```
81107

82-
## Empty-list Fallback in `{#each}`
108+
## `{#each}` — Keys and Empty-list Fallback
109+
110+
Always provide a key expression when the list or its items may change dynamically. This is especially critical when the block contains an inner `{#if}` — without a key, Svelte reuses DOM nodes by position, so filtering can silently bind data to the wrong element:
111+
112+
```svelte
113+
{#each workbooks as workbook (workbook.id)}
114+
{#if canRead(workbook)}
115+
<Row {workbook} />
116+
{/if}
117+
{/each}
118+
```
83119

84120
Use `{:else}` to render a placeholder when the list is empty — no wrapper conditional needed:
85121

@@ -91,6 +127,10 @@ Use `{:else}` to render a placeholder when the list is empty — no wrapper cond
91127
{/each}
92128
```
93129

130+
## Directory Structure: `list/` Subdirectories
131+
132+
Consider introducing a `list/` subdirectory (or other domain-scoped subdirectory) when the component count in a directory starts to feel unwieldy — roughly 20 files is a reasonable prompt to reconsider. Below that threshold, flat organization is preferred — subdirectories add navigation cost without proportional benefit.
133+
94134
## Eliminate Branching with Records
95135

96136
Replace `if`/ternary chains with `Record<EnumType, T>`:
@@ -103,3 +143,17 @@ const TAB_CONFIGS: Record<ActiveTab, TabConfig> = {
103143
```
104144

105145
Use the enum type as the key type, not `string`.
146+
147+
When not all enum keys need an entry, use `Partial<Record<K, V>>` as a **type annotation** — not `satisfies`. `as const satisfies Partial<Record<K, V>>` preserves the narrowed literal type, so indexing with other enum values causes a type error:
148+
149+
```typescript
150+
// NG: satisfies narrows the type — obj[key] errors for keys not in the literal
151+
const map = { [WorkBookType.SOLUTION]: SolutionTable }
152+
as const satisfies Partial<Record<WorkBookType, Component<Props>>>;
153+
154+
// OK: type annotation makes map[key] return Component<Props> | undefined
155+
const map: Partial<Record<WorkBookType, Component<Props>>> = {
156+
[WorkBookType.SOLUTION]: SolutionTable,
157+
};
158+
// Safe to guard with: {#if map[type]} or if (map[type])
159+
```

.claude/rules/svelte-docs.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
description: Svelte official documentation reference
3+
paths:
4+
- 'src/**/*.svelte'
5+
- 'src/**/*.ts'
6+
---
7+
8+
# Svelte Official Documentation
9+
10+
When Svelte 5 behavior is unclear, fetch the official docs directly via WebFetch instead of relying on training knowledge.
11+
12+
URL pattern: `https://svelte.dev/docs/svelte/{section}`
13+
14+
Examples:
15+
16+
- `$effect` behavior → `https://svelte.dev/docs/svelte/$effect`
17+
- Stores usage → `https://svelte.dev/docs/svelte/stores`
18+
- Runes overview → `https://svelte.dev/docs/svelte/what-are-runes`

.claude/rules/testing.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ paths:
99

1010
# Testing
1111

12+
## Test Titles
13+
14+
Write all test titles in English. Use descriptive sentences that state the expected behavior (e.g., `'returns empty array when workbooks is empty'`). Japanese is only acceptable in inline comments or fixture strings that represent real user-facing content.
15+
1216
## Test Integrity
1317

1418
- Never delete, comment out, or weaken assertions (e.g. `toEqual``toBeDefined`) to make tests pass
@@ -26,6 +30,7 @@ paths:
2630
- Use `toBe(true)` / `toBe(false)` over `toBeTruthy()` / `toBeFalsy()`
2731
- For DB query tests, assert `orderBy`, `include`, and other significant parameters with `expect.objectContaining` — not just `where`
2832
- Enum membership: `in` traverses the prototype chain; use `Object.hasOwn(Enum, value)` instead
33+
- **E2E state transitions**: after an interaction that changes element state (active tab, toggle, selection), assert the _new_ state — not just that the element is visible, which may have been true before the interaction. Assert an active CSS class, `aria-selected`, or similar attribute instead of `toBeVisible()`
2934

3035
## Cleanup in Tests
3136

@@ -45,19 +50,42 @@ try {
4550
- Use realistic fixture values (real task IDs, grade names) instead of placeholders like `'t1'`
4651
- Extract shared data into fixture files; inline is fine for single-use cases
4752
- After `.filter()` on fixtures, verify actual contents — same ID may refer to a different entity after fixture updates
53+
- **Description ↔ code path alignment**: when a test name describes a specific scenario (e.g. "tie-break"), verify the fixture actually exercises that code path. A test that passes without reaching the branch it claims to cover gives false confidence
4854

4955
## Mock Helpers
5056

51-
Extract repeated mock patterns into a helper in the test file:
57+
Extract repeated mock patterns into helpers in the test file. For Prisma service tests, define the return type alias once and use it across all helpers:
5258

5359
```typescript
54-
function mockFindMany(value: WorkBookPlacements) {
55-
vi.mocked(prisma.workBookPlacement.findMany).mockResolvedValue(
56-
value as unknown as Awaited<ReturnType<typeof prisma.workBookPlacement.findMany>>,
60+
type PrismaWorkBook = Awaited<ReturnType<typeof prisma.workBook.findUnique>>;
61+
type PrismaWorkBookRow = Awaited<ReturnType<typeof prisma.workBook.findMany>>[number];
62+
63+
function mockFindUnique(value: PrismaWorkBook) {
64+
vi.mocked(prisma.workBook.findUnique).mockResolvedValue(value);
65+
}
66+
67+
function mockFindMany(value: PrismaWorkBookRow[]) {
68+
vi.mocked(prisma.workBook.findMany).mockResolvedValue(
69+
value as unknown as Awaited<ReturnType<typeof prisma.workBook.findMany>>,
5770
);
5871
}
72+
73+
function mockCount(value: number) {
74+
vi.mocked(prisma.workBook.count).mockResolvedValue(value);
75+
}
5976
```
6077

78+
Extract `mockFindUnique`, `mockFindMany`, and `mockCount` as the standard trio for service tests that touch a single Prisma model. Add `mockCreate`, `mockTransaction`, and `mockDelete` when those operations are also tested.
79+
80+
## Component Vitest Unit Tests
81+
82+
Omit Vitest unit tests for a Svelte component when **both** conditions hold:
83+
84+
1. The component is template-only (no logic beyond prop bindings and basic conditionals)
85+
2. The component is covered by E2E tests
86+
87+
When a component contains extracted logic (e.g. derived values, event handlers, utility calls), add unit tests for that logic in the nearest `utils/` file instead of testing the component directly.
88+
6189
## Testing Extracted Utilities
6290

6391
- Add tests at extraction time, not later
@@ -81,3 +109,17 @@ Stop the split if internal helpers (e.g. `fetchUnplacedWorkbooks`) would be frag
81109
## HTTP Mocking
82110

83111
Use Nock for external HTTP calls. See `src/test/lib/clients/` for examples.
112+
113+
## Flowbite Toggle in E2E Tests
114+
115+
Flowbite's `Toggle` renders an `sr-only` `<input type="checkbox">` inside a `<label>`. Clicking the input directly fails because the visual `<span>` sibling intercepts pointer events. Click the label wrapper instead:
116+
117+
```typescript
118+
const toggleInput = page.locator('input[aria-label="<aria-label value>"]');
119+
const toggleLabel = page.locator('label:has(input[aria-label="<aria-label value>"])');
120+
121+
await toggleLabel.click();
122+
await expect(toggleInput).toBeChecked({ checked: true });
123+
```
124+
125+
The same pattern applies to any Flowbite component that visually overlays its native input (e.g. `Checkbox`, `Radio`).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: session-close
3+
description: Standard closing routine for an implementation session. Verifies tests, updates the plan checklist, proposes rule/skill additions, checks for bloat, and detects repeated instructions.
4+
disable-model-invocation: true
5+
argument-hint: '[plan-file-path]'
6+
---
7+
8+
Run the session-close routine described in [instructions.md](instructions.md).
9+
10+
Arguments: path to the active `plan.md` (optional — defaults to the most recently modified `docs/dev-notes/**/plan.md`).
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Session Close — Detailed Instructions
2+
3+
## Step 1: Verify Tests and Types
4+
5+
Run both checks and fix any failures before proceeding:
6+
7+
```bash
8+
pnpm test:unit
9+
pnpm test:integration
10+
pnpm check
11+
```
12+
13+
Only errors introduced by this session need fixing. Pre-existing errors (visible in git diff baseline) may be left as-is with a note.
14+
15+
## Step 2: Update plan.md
16+
17+
Target file: the path passed as `$ARGUMENTS`, or the most recently modified `docs/dev-notes/**/plan.md`.
18+
19+
- Mark completed tasks: `- [ ]``- [x]`
20+
- If all tasks in the plan are done, append a one-line completion note, then delete the file or replace its body with a single-line summary. Stale plan files must not be left behind.
21+
22+
## Step 3: Propose Rule / Skill Additions
23+
24+
Read all files in `.claude/rules/` and `.claude/skills/`. Then review the session's changes and identify lessons that meet **all** of the following criteria:
25+
26+
1. Generic enough to apply in future sessions (not specific to this PR's domain)
27+
2. Not already covered by an existing rule or skill
28+
3. Grounded in something that actually happened in this session (a bug caught, a type error, a pattern extracted)
29+
30+
Present each candidate as:
31+
32+
```
33+
→ Add to `<filename>`: under section `<section>`
34+
<code example>
35+
<one-sentence rationale>
36+
```
37+
38+
Do not apply changes until the user confirms.
39+
40+
## Step 4: Validate Rules / Skills for Bloat
41+
42+
For each file in `.claude/rules/`:
43+
44+
- Flag sections that duplicate content in another rule file
45+
- Flag files exceeding 150 lines where consolidation is possible
46+
- Flag outdated or project-specific content that no longer applies
47+
48+
Present a concrete diff proposal for each issue. Do not apply without confirmation.
49+
50+
## Step 5: Detect Repeated User Instructions
51+
52+
Scan the conversation history for instructions the user gave more than once across this or prior sessions (visible in memory or chat context). Categorize each as:
53+
54+
| Pattern | Suggested fix |
55+
| -------------------------------------- | ----------------------------------------- |
56+
| Always applies to every session | Add to `AGENTS.md` workflow |
57+
| Applies to a specific file type | Add to the relevant `.claude/rules/` file |
58+
| A multi-step workflow called on demand | Promote to a new Skill |
59+
60+
Report findings as a short bulleted list. Do not modify files without confirmation.

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ Always prefer simplicity over pathological correctness. YAGNI, KISS, DRY. No bac
1212
2. Before writing a new function, search `src/lib/utils/`, `src/lib/services/`, `src/features/*/utils/` and `src/features/*/services/` for existing implementations; extract shared logic there when it appears in 2+ places
1313
3. Write tests first, then implement production code, then verify with `pnpm test:unit`
1414
4. Review critically after implementing: flag YAGNI violations, over-abstraction, missing tests
15-
5. Record reusable insights in `.claude/rules/` or `docs/guides/` after the session
16-
6. Discard or summarize completed plans; don't leave stale TODOs
15+
5. Run `/session-close` at the end of each session: updates plan checklist, proposes rule/skill additions, checks for bloat, and detects repeated instructions
1716

1817
## Tech Stack
1918

@@ -69,9 +68,10 @@ prisma/schema.prisma # Database schema
6968
## Key Conventions
7069

7170
- **Svelte 5 Runes**: Use `$props()`, `$state()`, `$derived()` in all new components
71+
- **Service layer**: Services return data or `null`; never call `error()` or `redirect()`. HTTP error translation belongs in the route handler — the service must stay framework-agnostic and unit-testable.
7272
- **Server data**: `+page.server.ts``+page.svelte` via `data` prop
7373
- **Forms**: Superforms + Zod validation
74-
- **Tests**: Write tests before implementation (TDD). Use `@quramy/prisma-fabbrica` for factories, Nock for HTTP mocking
74+
- **Tests**: Write tests before implementation (TDD). Use `@quramy/prisma-fabbrica` for factories only in `prisma/seed.ts` and Playwright global setup (`tests/global-setup.ts`). For service-layer unit tests, mock the DB with `vi.mock('$lib/server/database', ...)` — do not use fabbrica there. Use Nock for HTTP mocking
7575
- **Naming**: `camelCase` variables, `PascalCase` types/components, `snake_case` files/routes, `kebab-case` directories
7676
- **Pre-commit**: Lefthook runs Prettier + ESLint (bypass: `LEFTHOOK=0 git commit`)
7777

docs/guides/claude-code.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,16 @@ paths:
6565

6666
**本プロジェクトの skills(`.claude/skills/`):**
6767

68-
| スキル | 用途 |
69-
| ---------------- | ---------------------------------------------------------------------- |
70-
| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) |
68+
| スキル | 用途 |
69+
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
70+
| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) |
71+
| `/session-close` | セッション終了時のルーティン:テスト確認 → plan.md 更新 → rules 候補提示 → 肥大化チェック → 繰り返し指示検出 |
72+
73+
**プロジェクトローカルスキルと `Skill` ツールの違い:**
74+
75+
- `Skill` ツール(Claude 内部のツール)はシステムプロンプトに列挙されたビルトインスキルのみ対象
76+
- `.claude/skills/` のプロジェクトローカルスキルは `/skill-name` スラッシュコマンドでのみ起動(Claude Code CLI が直接読み込む)
77+
- `/session-close` などを Claude に `Skill` ツール経由で呼ばせようとしても動作しない
7178

7279
### Hooks
7380

eslint.config.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ export default [
7171
},
7272
],
7373
// Add TypeScript ESLint rules manually
74-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
74+
'@typescript-eslint/no-unused-vars': [
75+
'error',
76+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
77+
],
7578
'@typescript-eslint/no-explicit-any': 'warn',
7679
// Disable some strict Svelte rules that are too aggressive
7780
'svelte/require-each-key': 'warn',

0 commit comments

Comments
 (0)