Skip to content

Commit 124478e

Browse files
KATO-Hiroclaude
andcommitted
docs/style: Update Prisma and Svelte component rules, fix TaskList
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent c4fd3c1 commit 124478e

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

.claude/rules/prisma-db.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ When a migration leaves `finished_at = NULL` in `_prisma_migrations`:
141141

142142
A `--rolled-back` migration is permanently skipped by `migrate deploy`; fixing the original file has no effect.
143143

144+
## Merged Data: Document Uniqueness Loss
145+
146+
Service functions that merge records from multiple sources (e.g., `getMergedTasksMap()` combining base tasks + `ContestTaskPair` entries) lose individual field uniqueness. Document that results contain duplicates in "unique" DB fields.
147+
148+
Example: `getMergedTasksMap()` returns multiple entries with same `task_id` but different `contest_id` — callers must not assume `task_id` is unique. UI layer must use composite keys (see svelte-components.md `{#each}` patterns).
149+
144150
## Validate Constraints
145151

146152
Prisma does not support `@@check`. To add one:

.claude/rules/svelte-components.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components:
3737
## `{#each}` Patterns
3838

3939
- Always key: `(item.id)` or `(i)`
40+
- **Key MUST be unique per iteration** — if domain allows duplicates, use composite key (e.g. `contest_id + task_id`)
4041
- Filter **before**, not inside with `{#if}`
4142
- Use `{:else}` for empty lists
4243

@@ -48,6 +49,8 @@ Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components:
4849
{/each}
4950
```
5051

52+
**Common Trap:** `task_id` alone is NOT unique when same task appears in multiple contests. Use `contest_id + '-' + task_id` as composite key (see issue #3460 & PR #3442).
53+
5154
## Snippets vs Components
5255

5356
- **Snippet**: needs `$state` access, pure display, same-file; params require explicit type `{#snippet label(item: Item)}`
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# グレード別一覧で展開されないグレードの修正
2+
3+
## 概要
4+
5+
[issue #3460](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3460): 問題一覧のグレード別タブで、一部グレードのアコーディオンが展開できない不具合を修正する。
6+
7+
## 原因調査
8+
9+
### 根本原因
10+
11+
`TaskList.svelte``{#each}` キーに `task_id` 単独を使用しているが、`task_id` はユニークではない場合がある。
12+
13+
`getMergedTasksMap()``src/lib/services/tasks.ts`)は同一の `task_id` を持ちながら `contest_id` が異なるタスク(同じ問題が複数コンテストに登場するケース)を生成する。`ContestTaskPair` モデルがこのケースを管理している。
14+
15+
```
16+
task_id: "typical90_s" contest_id: "typical90" ← baseTaskMap
17+
task_id: "typical90_s" contest_id: "tessoku-book" ← additionalTaskMap(ContestTaskPairで追加)
18+
```
19+
20+
この重複した `task_id` を持つ `TaskResult` が同一グレードに含まれると、`{#each}` のキーが重複する。Svelte の keyed `{#each}` は重複キーを正しく reconcile できず、`AccordionItem` の状態が不正になって展開できないグレードが発生する。
21+
22+
### PR #3442 との対比
23+
24+
PR #3442[#3442](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/3442))は同一構造のバグを修正:
25+
26+
- `TaskListForEdit.svelte``importTask.task_id``importTask.id`(真のユニークキーへ変更)
27+
28+
今回も同様に、真のユニーク識別子である `contest_id + task_id` の複合キーを使用する必要がある。
29+
30+
### 証拠
31+
32+
- `getMergedTasksMap` のコメント:「Handling cases where the same problem is used in different contests」
33+
- `TableBodyRow``id={taskResult.contest_id + '-' + taskResult.task_id}`:すでに複合キーで DOM id の一意性を確保済み
34+
35+
## `{#each}` キーの調査結果
36+
37+
コードベース全体で `{#each ... (key)}` パターンを持つ箇所を調査した結果:
38+
39+
| ファイル | キー | 判定 |
40+
| ---------------------------------------------------- | --------------------- | ----------------------------- |
41+
| `src/lib/components/TaskList.svelte:100` | `taskResult.task_id` | **要修正** |
42+
| `src/lib/components/TaskListSorted.svelte:37` | `taskResult.task_id` | 安全(Mapで重複排除済み) |
43+
| `src/routes/votes/+page.svelte:60` | `task.task_id` | 安全(DB `@unique` 制約あり) |
44+
| `src/routes/(admin)/vote_management/+page.svelte:39` | `stat.taskId` | 安全(集計テーブルのPK) |
45+
| その他 | `id` / enum値 / index | 安全 |
46+
47+
**`TaskListSorted.svelte`**`getTaskResultsOnlyResultExists()` が Map(`task_id` キー)で重複排除するため現時点では安全。ただし `getMergedTasksMap` を使うデータソースに変わった場合は壊れる可能性あり。
48+
49+
## 設計の判断
50+
51+
修正対象は `TaskList.svelte` のみ。`TableBodyRow``id` 属性が既に `contest_id + '-' + task_id` を使用しているため、同じセパレーターで統一する。
52+
53+
## 修正内容
54+
55+
**対象ファイル:** `src/lib/components/TaskList.svelte` (line 100)
56+
57+
```svelte
58+
<!-- Before -->
59+
{#each taskResults as taskResult (taskResult.task_id)}
60+
61+
<!-- After -->
62+
{#each taskResults as taskResult (taskResult.contest_id + '-' + taskResult.task_id)}
63+
```
64+
65+
## 検証方法
66+
67+
1. `pnpm test:unit` — ユニットテストの確認
68+
2. `pnpm dev` でローカルサーバーを起動し `/problems` → グレード別タブで全グレードが展開できることを確認
69+
3. 複数コンテストに登場する問題を含むグレード(例:典型90問、鉄則本)で特に確認

src/lib/components/TaskList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
</TableHead>
9898

9999
<TableBody class="divide-y divide-gray-200 dark:divide-gray-700">
100-
{#each taskResults as taskResult (taskResult.task_id)}
100+
{#each taskResults as taskResult (taskResult.contest_id + '-' + taskResult.task_id)}
101101
<TableBodyRow
102102
id={taskResult.contest_id + '-' + taskResult.task_id}
103103
class={getBackgroundColorFrom(taskResult.status_name)}

0 commit comments

Comments
 (0)