From a351433d82d1666d8fb4cd6f57ebaec44a034f72 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 05:50:43 +0000 Subject: [PATCH 1/6] feat: add NDPC ContestType with classifyContest and name label support Add Next DP Contest (NDPC) support: - NDPC enum value to Prisma schema - NDPC constant to ContestType object - classifyContest recognizes contest_id 'ndpc' - contestTypePriorities assigns priority 6 (after TDPC) - getContestNameLabel returns 'NDPC' - Test cases for all three functions Co-Authored-By: Claude Haiku 4.5 --- .../add-ndpc-contest-table-provider/plan.md | 442 ++++++++++++++++++ prisma/schema.prisma | 1 + prisma/tasks.ts | 140 ++++++ src/lib/types/contest.ts | 1 + src/lib/utils/contest.ts | 43 +- src/test/lib/utils/contest.test.ts | 24 + .../utils/test_cases/contest_name_labels.ts | 7 + src/test/lib/utils/test_cases/contest_type.ts | 7 + 8 files changed, 648 insertions(+), 17 deletions(-) create mode 100644 docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md diff --git a/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md b/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md new file mode 100644 index 000000000..1e17e810f --- /dev/null +++ b/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md @@ -0,0 +1,442 @@ +# Next DP Contest (NDPC) コンテストテーブル追加 実装計画 + +**Goal:** コンテストテーブルに Next DP Contest (NDPC) を追加し、EDPC・TDPC・FPS 24 グループに統合する。 + +**Architecture:** `ContestType.NDPC` を Prisma スキーマ → TypeScript 定数 → `classifyContest` / `contestTypePriorities` / `getContestNameLabel` → `NDPCProvider` → `dps` グループの順に追加する。TDD で各レイヤーを実装する。 + +**Tech Stack:** Prisma (PostgreSQL enum), TypeScript, Vitest + +--- + +## Context + +問題データ (`prisma/tasks.ts` 6349–6488 行) はすでに存在する (`contest_id: 'ndpc'`、問題 A–T の 20 問)。しかし `ContestType.NDPC` が未定義のため、コンテストテーブル上に表示されない。ユーザー指定の仕様: + +- 順番: `TDPC` の直後に配置 +- `dps` グループの buttonLabel: `EDPC・TDPC・NDPC・FPS 24` + +## 設計の根拠 + +- `NDPCProvider` は `TDPCProvider` と同じ Pattern 2 (Single Source) — `contest_id === 'ndpc'` による単一フィルター +- `contestTypePriorities` に NDPC=6 を割り当て、以後のエントリを全て1ずつ上げる (PAST=7, ACL_PRACTICE=8, ..., AOJ_JAG=23) + - 理由: 整数優先度の一貫性を保つ。浮動小数点は保守性を損なう。 +- `dp_providers.ts` に同居させることで DP 系プロバイダーをひとつのファイルに集約する (EDPC・TDPC と同じ理由) + +## 却下した代替案 + +- **独立ファイル `ndpc_provider.ts` に分離**: DP 系は `dp_providers.ts` に集約する既存慣習に反する。問題数の違いはプロバイダー実装に影響しない。 + +--- + +## 変更ファイル一覧 + +| ファイル | 操作 | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| `prisma/schema.prisma` | `ContestType` enum に `NDPC` 追加 | +| `src/lib/types/contest.ts` | `ContestType` オブジェクトに `NDPC` 追加 | +| `src/lib/utils/contest.ts` | `classifyContest` / `contestTypePriorities` / `getContestNameLabel` に NDPC 追記 | +| `src/test/lib/utils/test_cases/contest_type.ts` | `ndpc` テストケース追加 | +| `src/test/lib/utils/test_cases/contest_name_labels.ts` | `ndpc` テストケース追加 | +| `src/test/lib/utils/contest.test.ts` | 3 つの `describe` ブロック追加 | +| `src/features/tasks/utils/contest-table/dp_providers.ts` | `NDPCProvider` クラス追加 | +| `src/features/tasks/utils/contest-table/dp_providers.test.ts` | NDPC をパラメータ化テストに追加 | +| `src/features/tasks/utils/contest-table/contest_table_provider_groups.ts` | `dps` プリセット更新 | +| `src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts` | `dps` テスト更新 | + +--- + +## Task 1: Prisma スキーマ + マイグレーション + +**Files:** + +- Modify: `prisma/schema.prisma:307` + +- [ ] **Step 1: schema.prisma を編集** + +`TDPC // Typical DP Contest` の直後に追加: + +```prisma +NDPC // Next DP Contest +``` + +- [ ] **Step 2: マイグレーション実行** + +```bash +pnpm exec prisma migrate dev --name add_ndpc_to_contest_type +``` + +Expected: `Your database is now in sync with your schema.` のようなメッセージ。`@prisma/client` が再生成され `ContestTypeOrigin` に `NDPC` が含まれる。 + +- [ ] **Step 3: 型チェック** + +```bash +pnpm check +``` + +Expected: `src/lib/types/contest.ts` で型エラー(`NDPC` がオブジェクトに無い)。これは Task 2 で修正する。 + +--- + +## Task 2: TypeScript ContestType 定数更新 + +**Files:** + +- Modify: `src/lib/types/contest.ts:37` + +- [ ] **Step 1: NDPC を追加** + +```typescript +NDPC: 'NDPC', // Next DP Contest +``` + +`TDPC: 'TDPC', // Typical DP Contest` の直後に挿入。 + +- [ ] **Step 2: 型チェック** + +```bash +pnpm check +``` + +Expected: エラーなし(または NDPC 未使用の警告のみ)。 + +--- + +## Task 3: classifyContest / contestTypePriorities / getContestNameLabel 更新 (TDD) + +**Files:** + +- Modify: `src/test/lib/utils/test_cases/contest_type.ts` +- Modify: `src/test/lib/utils/test_cases/contest_name_labels.ts` +- Modify: `src/test/lib/utils/contest.test.ts` +- Modify: `src/lib/utils/contest.ts` + +- [ ] **Step 1: テストケースデータを追加 (contest_type.ts)** + +`tdpc` エクスポートの直後に追加: + +```typescript +export const ndpc = [ + createTestCaseForContestType('NDPC')({ + contestId: 'ndpc', + expected: ContestType.NDPC, + }), +]; +``` + +- [ ] **Step 2: テストケースデータを追加 (contest_name_labels.ts)** + +`tdpc` エクスポートの直後に追加: + +```typescript +export const ndpc = [ + createTestCaseForContestNameLabel('NDPC')({ + contestId: 'ndpc', + expected: 'NDPC', + }), +]; +``` + +- [ ] **Step 3: describe ブロックを追加 (contest.test.ts)** + +`classifyContest`・`getContestPriority`・`getContestNameLabel` の各 `when contest_id is tdpc` describe の直後に、それぞれ以下を追加: + +```typescript +describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(classifyContest(contestId)).toEqual(expected); + }); + }); +}); +``` + +```typescript +describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(getContestPriority(contestId)).toEqual(contestTypePriorities.get(expected)); + }); + }); +}); +``` + +```typescript +describe('when contest_id is ndpc', () => { + TestCasesForContestNameLabel.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { + expect(getContestNameLabel(contestId)).toEqual(expected); + }); + }); +}); +``` + +- [ ] **Step 4: テスト失敗を確認** + +```bash +pnpm test:unit src/test/lib/utils/contest.test.ts +``` + +Expected: NDPC 関連テストが FAIL。 + +- [ ] **Step 5: contest.ts を実装** + +`classifyContest` — `if (contest_id === 'tdpc')` ブロックの直後に追加: + +```typescript +if (contest_id === 'ndpc') { + return ContestType.NDPC; +} +``` + +`contestTypePriorities` — 全体の優先度を調整: + +- `[ContestType.NDPC, 6]` を TDPC の直後に追加 +- 以後のエントリを全て 1 ずつ上げる (PAST: 6→7, ACL_PRACTICE: 7→8, ..., AOJ_JAG: 22→23) + +新しい優先度マップ: + +```typescript +[ContestType.ABS, 0], +[ContestType.ABC, 1], +[ContestType.APG4B, 2], +[ContestType.TYPICAL90, 3], +[ContestType.EDPC, 4], +[ContestType.TDPC, 5], +[ContestType.NDPC, 6], // <- 新規 +[ContestType.PAST, 7], // <- 6→7 +[ContestType.ACL_PRACTICE, 8], // <- 7→8 +[ContestType.JOI, 9], // <- 8→9 +[ContestType.TESSOKU_BOOK, 10], // <- 9→10 +[ContestType.MATH_AND_ALGORITHM, 11], // <- 10→11 +[ContestType.ARC, 12], // <- 11→12 +[ContestType.AGC, 13], // <- 12→13 +[ContestType.ABC_LIKE, 14], // <- 13→14 +[ContestType.ARC_LIKE, 15], // <- 14→15 +[ContestType.AGC_LIKE, 16], // <- 15→16 +[ContestType.AWC, 17], // <- 16→17 +[ContestType.UNIVERSITY, 18], // <- 17→18 +[ContestType.FPS_24, 19], // <- 18→19 +[ContestType.OTHERS, 20], // <- 19→20 +[ContestType.AOJ_COURSES, 21], // <- 20→21 +[ContestType.AOJ_PCK, 22], // <- 21→22 +[ContestType.AOJ_JAG, 23], // <- 22→23 +``` + +`getContestNameLabel` — `if (contestId === 'tdpc') { return 'TDPC'; }` の直後に追加: + +```typescript +if (contestId === 'ndpc') { + return 'NDPC'; +} +``` + +- [ ] **Step 6: テスト通過を確認** + +```bash +pnpm test:unit src/test/lib/utils/contest.test.ts +``` + +Expected: すべて PASS。 + +- [ ] **Step 7: コミット** + +```bash +git add prisma/schema.prisma prisma/migrations/ src/lib/types/contest.ts src/lib/utils/contest.ts src/test/lib/utils/test_cases/contest_type.ts src/test/lib/utils/test_cases/contest_name_labels.ts src/test/lib/utils/contest.test.ts +git commit -m "feat: add NDPC ContestType with classifyContest and name label support" +``` + +--- + +## Task 4: NDPCProvider 実装 (TDD) + +**Files:** + +- Modify: `src/features/tasks/utils/contest-table/dp_providers.test.ts` +- Modify: `src/features/tasks/utils/contest-table/dp_providers.ts` + +- [ ] **Step 1: テストを追加 (dp_providers.test.ts)** + +`describe.each` の配列に NDPC エントリを追加: + +```typescript +{ + providerClass: NDPCProvider, + contestType: ContestType.NDPC, + contestId: 'ndpc', + title: 'Next DP Contest', + abbreviationName: 'ndpc', + label: 'NDPC provider', +}, +``` + +import 行も更新: + +```typescript +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; +``` + +- [ ] **Step 2: テスト失敗を確認** + +```bash +pnpm test:unit src/features/tasks/utils/contest-table/dp_providers.test.ts +``` + +Expected: `NDPCProvider` が未定義で FAIL。 + +- [ ] **Step 3: NDPCProvider を実装 (dp_providers.ts)** + +`TDPCProvider` クラスの直後に追加: + +```typescript +export class NDPCProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + + return taskResult.contest_id === 'ndpc'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'Next DP Contest', + abbreviationName: 'ndpc', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(_contestId: string): string { + return ''; + } +} +``` + +- [ ] **Step 4: テスト通過を確認** + +```bash +pnpm test:unit src/features/tasks/utils/contest-table/dp_providers.test.ts +``` + +Expected: すべて PASS。 + +--- + +## Task 5: dps グループに NDPCProvider を登録 + +**Files:** + +- Modify: `src/features/tasks/utils/contest-table/contest_table_provider_groups.ts:26,187-195` +- Modify: `src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts` + +- [ ] **Step 1: テストを更新 (contest_table_provider_groups.test.ts)** + +`expects to create DPs preset correctly` テストを更新: + +```typescript +test('expects to create DPs preset correctly', () => { + const group = prepareContestProviderPresets().dps(); + + expect(group.getGroupName()).toBe('EDPC・TDPC・NDPC・FPS 24'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', + }); + expect(group.getSize()).toBe(4); + expect(group.getProvider(ContestType.EDPC)).toBeInstanceOf(EDPCProvider); + expect(group.getProvider(ContestType.TDPC)).toBeInstanceOf(TDPCProvider); + expect(group.getProvider(ContestType.NDPC)).toBeInstanceOf(NDPCProvider); + expect(group.getProvider(ContestType.FPS_24)).toBeInstanceOf(FPS24Provider); +}); +``` + +import 行に `NDPCProvider` を追加。 + +- [ ] **Step 2: テスト失敗を確認** + +```bash +pnpm test:unit src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +``` + +Expected: グループ名・サイズ・NDPC provider のアサーションが FAIL。 + +- [ ] **Step 3: contest_table_provider_groups.ts を更新** + +import 行を更新: + +```typescript +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; +``` + +`dps` プリセットを更新: + +```typescript +dps: () => + new ContestTableProviderGroup(`EDPC・TDPC・NDPC・FPS 24`, { + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', + }).addProviders( + new EDPCProvider(ContestType.EDPC), + new TDPCProvider(ContestType.TDPC), + new NDPCProvider(ContestType.NDPC), + new FPS24Provider(ContestType.FPS_24), + ), +``` + +- [ ] **Step 4: テスト通過を確認** + +```bash +pnpm test:unit src/features/tasks/utils/contest-table/ +``` + +Expected: すべて PASS。 + +- [ ] **Step 5: 全テストを実行** + +```bash +pnpm test:unit +``` + +Expected: すべて PASS。 + +- [ ] **Step 6: 型チェック** + +```bash +pnpm check +``` + +Expected: エラーなし。 + +- [ ] **Step 7: コミット** + +```bash +git add src/features/tasks/utils/contest-table/dp_providers.ts src/features/tasks/utils/contest-table/dp_providers.test.ts src/features/tasks/utils/contest-table/contest_table_provider_groups.ts src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +git commit -m "feat: add NDPCProvider and register in dps contest group" +``` + +--- + +## Verification + +```bash +# 全テスト +pnpm test:unit + +# 型チェック +pnpm check + +# lint +pnpm lint +``` + +期待動作: `pnpm dev` 起動後、コンテストテーブルのフィルターに `EDPC・TDPC・NDPC・FPS 24` ボタンが表示され、NDPC の 20 問 (A–T) が表示される。 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9fcf7c7c5..0f2269f82 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -305,6 +305,7 @@ enum ContestType { PAST // Practical Algorithm Skill Test (アルゴリズム実技検定) EDPC // Educational DP Contest / DP まとめコンテスト TDPC // Typical DP Contest + NDPC // Next DP Contest JOI // Japanese Olympiad in Informatics TYPICAL90 // 競プロ典型 90 問 TESSOKU_BOOK // 競技プログラミングの鉄則 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 8f8de9da5..e2b795a1a 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -6346,6 +6346,146 @@ export const tasks = [ title: 'A. コンテスト', grade: 'Q2', }, + { + id: 'ndpc2026_t', + contest_id: 'ndpc', + problem_index: 'T', + name: 'Independent Set', + title: 'T. Independent Set', + }, + { + id: 'ndpc2026_s', + contest_id: 'ndpc', + problem_index: 'S', + name: 'Two doors', + title: 'S. Two doors', + }, + { + id: 'ndpc2026_r', + contest_id: 'ndpc', + problem_index: 'R', + name: 'Triples', + title: 'R. Triples', + }, + { + id: 'ndpc2026_q', + contest_id: 'ndpc', + problem_index: 'Q', + name: 'Union of Intervals', + title: 'Q. Union of Intervals', + }, + { + id: 'ndpc2026_p', + contest_id: 'ndpc', + problem_index: 'P', + name: 'LIS', + title: 'P. LIS', + }, + { + id: 'ndpc2026_o', + contest_id: 'ndpc', + problem_index: 'O', + name: 'Game', + title: 'O. Game', + }, + { + id: 'ndpc2026_n', + contest_id: 'ndpc', + problem_index: 'N', + name: 'Knapsack', + title: 'N. Knapsack', + }, + { + id: 'ndpc2026_m', + contest_id: 'ndpc', + problem_index: 'M', + name: 'Numeral', + title: 'M. Numeral', + }, + { + id: 'ndpc2026_l', + contest_id: 'ndpc', + problem_index: 'L', + name: 'LCM', + title: 'L. LCM', + }, + { + id: 'ndpc2026_k', + contest_id: 'ndpc', + problem_index: 'K', + name: 'Addition and Subtraction', + title: 'K. Addition and Subtraction', + }, + { + id: 'ndpc2026_j', + contest_id: 'ndpc', + problem_index: 'J', + name: 'Number and Total', + title: 'J. Number and Total', + }, + { + id: 'ndpc2026_i', + contest_id: 'ndpc', + problem_index: 'I', + name: 'Update Positions', + title: 'I. Update Positions', + }, + { + id: 'ndpc2026_h', + contest_id: 'ndpc', + problem_index: 'H', + name: 'Coin', + title: 'H. Coin', + }, + { + id: 'ndpc2026_g', + contest_id: 'ndpc', + problem_index: 'G', + name: 'Mouth', + title: 'G. Mouth', + }, + { + id: 'ndpc2026_f', + contest_id: 'ndpc', + problem_index: 'F', + name: 'Set', + title: 'F. Set', + }, + { + id: 'ndpc2026_e', + contest_id: 'ndpc', + problem_index: 'E', + name: 'Summer Vacation', + title: 'E. Summer Vacation', + }, + { + id: 'ndpc2026_d', + contest_id: 'ndpc', + problem_index: 'D', + name: 'Banknote', + title: 'D. Banknote', + }, + { + id: 'ndpc2026_c', + contest_id: 'ndpc', + problem_index: 'C', + name: 'String', + title: 'C. String', + }, + { + id: 'ndpc2026_b', + contest_id: 'ndpc', + problem_index: 'B', + name: 'DAG', + title: 'B. DAG', + }, + { + id: 'ndpc2026_a', + contest_id: 'ndpc', + problem_index: 'A', + name: 'Polyomino', + title: 'A. Polyomino', + }, { id: 'math_and_algorithm_bn', contest_id: 'math-and-algorithm', diff --git a/src/lib/types/contest.ts b/src/lib/types/contest.ts index c44adbaf4..270ead6e2 100644 --- a/src/lib/types/contest.ts +++ b/src/lib/types/contest.ts @@ -35,6 +35,7 @@ export const ContestType: { [key in ContestTypeOrigin]: key } = { PAST: 'PAST', // Practical Algorithm Skill Test (アルゴリズム実技検定) EDPC: 'EDPC', // Educational DP Contest / DP まとめコンテスト TDPC: 'TDPC', // Typical DP Contest + NDPC: 'NDPC', // Next DP Contest JOI: 'JOI', // Japanese Olympiad in Informatics TYPICAL90: 'TYPICAL90', // 競プロ典型 90 問 TESSOKU_BOOK: 'TESSOKU_BOOK', // 競技プログラミングの鉄則 diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 186ecf30a..eec49444b 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -40,6 +40,10 @@ export const classifyContest = (contest_id: string) => { return ContestType.TDPC; } + if (contest_id === 'ndpc') { + return ContestType.NDPC; + } + if (contest_id.startsWith('past')) { return ContestType.PAST; } @@ -270,23 +274,24 @@ export const contestTypePriorities: Map = new Map([ [ContestType.TYPICAL90, 3], [ContestType.EDPC, 4], [ContestType.TDPC, 5], - [ContestType.PAST, 6], - [ContestType.ACL_PRACTICE, 7], - [ContestType.JOI, 8], - [ContestType.TESSOKU_BOOK, 9], - [ContestType.MATH_AND_ALGORITHM, 10], - [ContestType.ARC, 11], - [ContestType.AGC, 12], - [ContestType.ABC_LIKE, 13], - [ContestType.ARC_LIKE, 14], - [ContestType.AGC_LIKE, 15], - [ContestType.AWC, 16], - [ContestType.UNIVERSITY, 17], - [ContestType.FPS_24, 18], - [ContestType.OTHERS, 19], // AtCoder (その他) - [ContestType.AOJ_COURSES, 20], - [ContestType.AOJ_PCK, 21], - [ContestType.AOJ_JAG, 22], + [ContestType.NDPC, 6], + [ContestType.PAST, 7], + [ContestType.ACL_PRACTICE, 8], + [ContestType.JOI, 9], + [ContestType.TESSOKU_BOOK, 10], + [ContestType.MATH_AND_ALGORITHM, 11], + [ContestType.ARC, 12], + [ContestType.AGC, 13], + [ContestType.ABC_LIKE, 14], + [ContestType.ARC_LIKE, 15], + [ContestType.AGC_LIKE, 16], + [ContestType.AWC, 17], + [ContestType.UNIVERSITY, 18], + [ContestType.FPS_24, 19], + [ContestType.OTHERS, 20], // AtCoder (その他) + [ContestType.AOJ_COURSES, 21], + [ContestType.AOJ_PCK, 22], + [ContestType.AOJ_JAG, 23], ]); export function getContestPriority(contestId: string): number { @@ -377,6 +382,10 @@ export const getContestNameLabel = (contestId: string) => { return 'TDPC'; } + if (contestId === 'ndpc') { + return 'NDPC'; + } + if (contestId.startsWith('past')) { return getPastContestLabel(PAST_TRANSLATIONS, contestId); } diff --git a/src/test/lib/utils/contest.test.ts b/src/test/lib/utils/contest.test.ts index 3267d11aa..d044131cf 100644 --- a/src/test/lib/utils/contest.test.ts +++ b/src/test/lib/utils/contest.test.ts @@ -67,6 +67,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(classifyContest(contestId)).toEqual(expected); + }); + }); + }); + describe('when contest_id contains past', () => { TestCasesForContestType.past.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { @@ -257,6 +265,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(getContestPriority(contestId)).toEqual(contestTypePriorities.get(expected)); + }); + }); + }); + describe('when contest_id contains past', () => { TestCasesForContestType.past.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { @@ -415,6 +431,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestNameLabel.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { + expect(getContestNameLabel(contestId)).toEqual(expected); + }); + }); + }); + describe('when contest_id is practice2 (ACL practice)', () => { TestCasesForContestNameLabel.aclPractice.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { diff --git a/src/test/lib/utils/test_cases/contest_name_labels.ts b/src/test/lib/utils/test_cases/contest_name_labels.ts index 30832f6b9..7ce7a8f4a 100644 --- a/src/test/lib/utils/test_cases/contest_name_labels.ts +++ b/src/test/lib/utils/test_cases/contest_name_labels.ts @@ -21,6 +21,13 @@ export const tdpc = [ }), ]; +export const ndpc = [ + createTestCaseForContestNameLabel('NDPC')({ + contestId: 'ndpc', + expected: 'NDPC', + }), +]; + export const aclPractice = [ createTestCaseForContestNameLabel('ACL Practice')({ contestId: 'practice2', diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 81b7fdb4d..7e3d10fe9 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -62,6 +62,13 @@ export const tdpc = [ }), ]; +export const ndpc = [ + createTestCaseForContestType('NDPC')({ + contestId: 'ndpc', + expected: ContestType.NDPC, + }), +]; + const pastContestData = [ { name: 'PAST 1st', contestId: 'past201912-open' }, { name: 'PAST 2nd', contestId: 'past202004-open' }, From a21ccf4759ef7cd15c929cf8dfd6cb8b0c05e0d6 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 05:58:42 +0000 Subject: [PATCH 2/6] feat: add NDPCProvider and register in dps contest group Register NDPC in DP contests group: - NDPCProvider implemented to filter NDPC tasks - dps preset now includes NDPC alongside EDPC, TDPC, and FPS 24 - Update button label and aria-label to reflect 4 DP contests - Update affected test cases for priority ordering Co-Authored-By: Claude Haiku 4.5 --- .../contest_table_provider_groups.test.ts | 10 +++--- .../contest_table_provider_groups.ts | 11 ++++--- .../utils/contest-table/dp_providers.test.ts | 10 +++++- .../tasks/utils/contest-table/dp_providers.ts | 33 +++++++++++++++++++ src/test/lib/utils/task.test.ts | 10 +++--- 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts index 979271046..40f5861d6 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts @@ -20,6 +20,7 @@ import { ACLProvider, EDPCProvider, TDPCProvider, + NDPCProvider, FPS24Provider, JOIFirstQualRoundProvider, JOISecondQualRound2020OnwardsProvider, @@ -228,14 +229,15 @@ describe('prepareContestProviderPresets', () => { test('expects to create DPs preset correctly', () => { const group = prepareContestProviderPresets().dps(); - expect(group.getGroupName()).toBe('EDPC・TDPC・FPS 24'); + expect(group.getGroupName()).toBe('EDPC・TDPC・NDPC・FPS 24'); expect(group.getMetadata()).toEqual({ - buttonLabel: 'EDPC・TDPC・FPS 24', - ariaLabel: 'EDPC and TDPC and FPS 24 contests', + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', }); - expect(group.getSize()).toBe(3); + expect(group.getSize()).toBe(4); expect(group.getProvider(ContestType.EDPC)).toBeInstanceOf(EDPCProvider); expect(group.getProvider(ContestType.TDPC)).toBeInstanceOf(TDPCProvider); + expect(group.getProvider(ContestType.NDPC)).toBeInstanceOf(NDPCProvider); expect(group.getProvider(ContestType.FPS_24)).toBeInstanceOf(FPS24Provider); }); diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts index af8aa961d..4504182ca 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts @@ -23,7 +23,7 @@ import { TessokuBookForChallengesProvider, } from './tessoku_book_providers'; import { MathAndAlgorithmProvider } from './math_and_algorithm_provider'; -import { EDPCProvider, TDPCProvider } from './dp_providers'; +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; import { FPS24Provider } from './fps24_provider'; import { ACLPracticeProvider, ACLBeginnerProvider, ACLProvider } from './acl_providers'; import { @@ -182,15 +182,16 @@ export const prepareContestProviderPresets = () => { }).addProvider(new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM)), /** - * DP group (EDPC and TDPC) + * DP group (EDPC, TDPC, NDPC, and FPS 24) */ dps: () => - new ContestTableProviderGroup(`EDPC・TDPC・FPS 24`, { - buttonLabel: 'EDPC・TDPC・FPS 24', - ariaLabel: 'EDPC and TDPC and FPS 24 contests', + new ContestTableProviderGroup(`EDPC・TDPC・NDPC・FPS 24`, { + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', }).addProviders( new EDPCProvider(ContestType.EDPC), new TDPCProvider(ContestType.TDPC), + new NDPCProvider(ContestType.NDPC), new FPS24Provider(ContestType.FPS_24), ), diff --git a/src/features/tasks/utils/contest-table/dp_providers.test.ts b/src/features/tasks/utils/contest-table/dp_providers.test.ts index 68556daaa..92cc9407c 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.test.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect } from 'vitest'; import { ContestType } from '$lib/types/contest'; import type { TaskResults } from '$lib/types/task'; -import { EDPCProvider, TDPCProvider } from './dp_providers'; +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; describe('DP providers', () => { describe.each([ @@ -23,6 +23,14 @@ describe('DP providers', () => { abbreviationName: 'tdpc', label: 'TDPC provider', }, + { + providerClass: NDPCProvider, + contestType: ContestType.NDPC, + contestId: 'ndpc', + title: 'Next DP Contest', + abbreviationName: 'ndpc', + label: 'NDPC provider', + }, ])('$label', ({ providerClass, contestType, contestId, title, abbreviationName }) => { test('expects to get correct metadata', () => { const provider = new providerClass(contestType); diff --git a/src/features/tasks/utils/contest-table/dp_providers.ts b/src/features/tasks/utils/contest-table/dp_providers.ts index 8d1d84c91..8ee49b01f 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.ts @@ -73,3 +73,36 @@ export class TDPCProvider extends ContestTableProviderBase { return ''; } } + +export class NDPCProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + + return taskResult.contest_id === 'ndpc'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'Next DP Contest', + abbreviationName: 'ndpc', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(_contestId: string): string { + return ''; + } +} diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index 272fd3974..6c9f8c4f2 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -368,27 +368,27 @@ describe('Task', () => { { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.past202309_a, - expected: -5, // order: abc999_a, past202309_a + expected: -6, // order: abc999_a, past202309_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.acl_a, - expected: -6, // order: abc999_a, acl_a + expected: -7, // order: abc999_a, acl_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.joi2023_yo1c, - expected: -7, // order: abc999_a, joi2023_yo1c + expected: -8, // order: abc999_a, joi2023_yo1c }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.tessoku_book_a, - expected: -8, // order: abc999_a, tessoku_book_a + expected: -9, // order: abc999_a, tessoku_book_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.math_and_algorithm_a, - expected: -9, // order: abc999_a, math_and_algorithm_a + expected: -10, // order: abc999_a, math_and_algorithm_a }, ]; From 34be160e5a8a8078583afacaf95e881aaf119154 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 06:12:49 +0000 Subject: [PATCH 3/6] fix: restore original contestTypePriorities comment categories Revert to Educational/Genius/Special/External classification. Update priority ranges after NDPC insertion at priority 6: - Educational contests (0-11, 17) - Contests for genius (12-16) - Special contests (18-20) - External platforms (21-23) Also fix single-char lambda parameter in docs code example. Co-Authored-By: Claude Haiku 4.5 --- .../guides/how-to-add-contest-table-provider.md | 7 +++++-- src/lib/utils/contest.ts | 10 +++++----- src/test/lib/utils/task.test.ts | 7 ++++++- src/test/lib/utils/test_cases/task_results.ts | 17 ++++++++++++++++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index 5eba16fa5..28886874c 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -265,12 +265,14 @@ class TessokuBookSectionProvider extends TessokuBookProvider { | -------------- | ------------- | ---------- | ------------ | | EDPC | `'dp'` | 26問 | A~Z | | TDPC | `'tdpc'` | 26問 | A~Z | +| NDPC | `'ndpc'` | 20問 | A~T | | FPS_24 | `'fps-24'` | 24問 | A~X | | ACL_PRACTICE | `'practice2'` | 12問 | A~L | | ACL_BEGINNER\* | `'abl'` | 6問 | A~F | | ACL_CONTEST1\* | `'acl1'` | 6問 | A~F | \*注: ACL_PRACTICE、ACL_BEGINNER、ACL_CONTEST1 は `Acl` グループの下で 3 つのコンテストが統一管理されています。 +\*\*注: EDPC・TDPC・NDPC・FPS 24 は `dps` グループ下で 4 つのコンテストが統一管理されています。 ### 複合ソース型 @@ -317,7 +319,7 @@ describe('MyNewProvider', () => { test('filters tasks correctly', () => { const provider = new MyNewProvider(ContestType.MY_NEW); const filtered = provider.filter(taskResultsForMyNew); - expect(filtered.every((t) => t.contest_id === 'my-contest')).toBe(true); + expect(filtered.every((task) => task.contest_id === 'my-contest')).toBe(true); }); test('returns correct metadata', () => { @@ -532,6 +534,7 @@ describe('CustomProvider with unique config', () => { - [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider - [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920)、[#3120](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3120) - ACLPracticeProvider、ACLBeginnerProvider、ACLProvider - [#3152](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3152) - JOISemiFinalRoundProvider(本選 → セミファイナルステージ への対応) +- NDPC実装 - NDPCProvider(パターン2: 単一ソース型、prisma/tasks.ts に 20 問存在) ### 実装ファイル @@ -541,4 +544,4 @@ describe('CustomProvider with unique config', () => { --- -**最終更新**: 2026-02-22 +**最終更新**: 2026-05-10 diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index eec49444b..ba291fe2f 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -252,13 +252,13 @@ export function getContestPrefixes(contestPrefixes: Record) { } /** - * Contest type priorities (0 = Highest, 21 = Lowest) + * Contest type priorities (0 = Highest, 23 = Lowest) * * Priority assignment rationale: - * - Educational contests (0-10, 16): ABS, ABC, APG4B and AWC etc. - * - Contests for genius (11-15): ARC, AGC, and their variants - * - Special contests (17-19): UNIVERSITY, FPS_24, OTHERS - * - External platforms (20-22): AOJ_COURSES, AOJ_PCK, AOJ_JAG + * - Educational contests (0-11, 17): ABS, ABC, APG4B and AWC etc. + * - Contests for genius (12-16): ARC, AGC, and their variants + * - Special contests (18-20): UNIVERSITY, FPS_24, OTHERS + * - External platforms (21-23): AOJ_COURSES, AOJ_PCK, AOJ_JAG * * @remarks * HACK: The priorities for ARC, AGC, UNIVERSITY, AOJ_COURSES, and AOJ_PCK are temporary diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index 6c9f8c4f2..e41680037 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -363,7 +363,12 @@ describe('Task', () => { { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.tdpc_contest, - expected: -4, // order: abc999_a, tpdc_contest + expected: -4, // order: abc999_a, tdpc_contest + }, + { + first: tasksForVerificationOfOrder.abc999_a, + second: tasksForVerificationOfOrder.ndpc2026_a, + expected: -5, // order: abc999_a, ndpc2026_a }, { first: tasksForVerificationOfOrder.abc999_a, diff --git a/src/test/lib/utils/test_cases/task_results.ts b/src/test/lib/utils/test_cases/task_results.ts index 565de0401..253ce4393 100644 --- a/src/test/lib/utils/test_cases/task_results.ts +++ b/src/test/lib/utils/test_cases/task_results.ts @@ -405,6 +405,20 @@ const tdpc_contest: TaskResult = { grade: 'PENDING', updated_at: new Date(), }; +const ndpc2026_a: TaskResult = { + is_ac: false, + user_id: userId2, + status_name: 'ns', + status_id: '4', + submission_status_image_path: 'ns.png', + submission_status_label_name: '未挑戦', + contest_id: 'ndpc', + task_table_index: 'A', + task_id: 'ndpc2026_a', + title: 'A. Polyomino', + grade: 'PENDING', + updated_at: new Date(), +}; const acl_a: TaskResult = { is_ac: false, user_id: userId2, @@ -490,8 +504,9 @@ export const tasksForVerificationOfOrder = { typical90_a, dp_b, tdpc_contest, - acl_a, + ndpc2026_a, past202309_a, + acl_a, joi2023_yo1c, tessoku_book_a, math_and_algorithm_a, From deaf44b9b5caea25bd1f8af24f41ce0ad946e8c0 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 06:46:00 +0000 Subject: [PATCH 4/6] docs: add add-contest-table-provider skill and documentation updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /add-contest-table-provider skill with TDD-based implementation guide - Add comprehensive checklist for 5-layer implementation (Prisma → types → utils → providers → groups) - Document 3 implementation patterns (range filter, single source, composite) - Update how-to-add-contest-table-provider with common mistake #5 (contestTypePriorities JSDoc categories) - Update claude-code.md with new skill reference Co-Authored-By: Claude Haiku 4.5 --- .../add-contest-table-provider/SKILL.md | 13 ++ .../instructions.md | 123 ++++++++++++++++++ docs/guides/claude-code.md | 13 +- .../how-to-add-contest-table-provider.md | 27 +++- 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/add-contest-table-provider/SKILL.md create mode 100644 .claude/skills/add-contest-table-provider/instructions.md diff --git a/.claude/skills/add-contest-table-provider/SKILL.md b/.claude/skills/add-contest-table-provider/SKILL.md new file mode 100644 index 000000000..5df8a11d9 --- /dev/null +++ b/.claude/skills/add-contest-table-provider/SKILL.md @@ -0,0 +1,13 @@ +--- +name: add-contest-table-provider +description: Add a new ContestType and ContestTableProvider across 5 layers using TDD. Covers all 3 patterns. Asks targeted questions to gather pattern-specific requirements before touching code. +argument-hint: ' ' +--- + +Add a new contest table provider for: $ARGUMENTS + +> When in doubt at any step, use AskUserQuestion before proceeding. + +0. **Seed check** — grep `prisma/tasks.ts` for the contest_id(s); report count + task_ids; if absent or incomplete, ask the user to add missing rows (reference: `https://kenkoooo.com/atcoder/resources/problems.json`) +1. **Gather requirements** — infer the implementation pattern; confirm per [instructions.md §Requirements](instructions.md) +2. **Implement** — follow [instructions.md](instructions.md) for the confirmed pattern across 5 layers (TDD) diff --git a/.claude/skills/add-contest-table-provider/instructions.md b/.claude/skills/add-contest-table-provider/instructions.md new file mode 100644 index 000000000..28bdce16f --- /dev/null +++ b/.claude/skills/add-contest-table-provider/instructions.md @@ -0,0 +1,123 @@ +# Add Contest Table Provider — Implementation Checklist + +Reference: `docs/guides/how-to-add-contest-table-provider.md` + +--- + +## Requirements gathering + +Step 0 (seed check) is already done. Confirm the following before touching code: + +**All patterns:** + +- Which pattern? (State your inference from the data, ask to confirm) + - Pattern 1: numeric range filter (e.g. ABC 001–041) + - Pattern 2: single fixed contest_id (e.g. NDPC, TDPC, FPS_24) + - Pattern 3: multiple contest_ids unified in one table (e.g. ABS, ABC-Like) +- Nearest neighbor ContestType for insertion order in `contestTypePriorities`? +- New group or merge into existing? If new: group name / `buttonLabel` / `ariaLabel`? + +**Pattern 1 additional:** + +- Numeric range: start and end (open-ended if no upper bound)? +- Shared problems with another contest (e.g. ARC–ABC overlap)? Which contest_ids appear in both? +- Round label format (e.g. `ABC 042`)? + +**Pattern 3 additional:** + +- Show the full contest_id list found in `prisma/tasks.ts` — any missing or to exclude? +- Does `prisma/contest_task_pairs.ts` need updating (shared task_ids across contests)? +- task_table_index format: numeric (`001–`) or alphabetic (`A–`)? +- Section splits needed? If yes: split key and section names? + +--- + +## Layer 1 — Prisma schema + +- [ ] Add `XXX // Full Contest Name` to `prisma/schema.prisma` ContestType enum (after nearest neighbor) +- [ ] `pnpm exec prisma generate` — non-interactive env; `migrate dev` requires interactive shell +- [ ] `pnpm check` — expect a type error in `src/lib/types/contest.ts` (confirms client regenerated) + +## Layer 2 — TypeScript ContestType constant + +- [ ] Add `XXX: 'XXX', // Full Contest Name` to `ContestType` in `src/lib/types/contest.ts` (same position as schema) +- [ ] `pnpm check` — error should be gone + +## Layer 3 — Contest utilities (TDD) + +### Write tests first + +- [ ] Add export to `src/test/lib/utils/test_cases/contest_type.ts` (after nearest neighbor) +- [ ] Add export to `src/test/lib/utils/test_cases/contest_name_labels.ts` (after nearest neighbor) +- [ ] Add three `describe('when contest_id is xxx')` blocks to `src/test/lib/utils/contest.test.ts`: + - under `classify contest` + - under `get contest priority` + - under `get contest name label` +- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect RED** + +### Implement + +- [ ] Add `classifyContest` branch after nearest neighbor's branch in `src/lib/utils/contest.ts` +- [ ] Insert `[ContestType.XXX, N]` into `contestTypePriorities` after nearest neighbor + - All entries after the insertion point shift by +1 + - **Update the JSDoc numeric ranges** — do NOT rename or split the existing four categories + (Educational / Contests for genius / Special contests / External platforms) + - **Search `src/test/lib/utils/task.test.ts` for hardcoded priority-diff expected values** + and decrement by 1 for every ContestType that shifted +- [ ] Add `getContestNameLabel` branch after nearest neighbor's branch +- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect GREEN** + +--- + +## Layer 4 — Provider class (TDD) + +### Pattern 2: single source + +- [ ] Add entry to `describe.each` array in `dp_providers.test.ts` (or the appropriate `*_providers.test.ts`) +- [ ] Add import of new Provider class +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider class in the appropriate `*_providers.ts` after nearest neighbor +- [ ] `pnpm test:unit ` — **expect GREEN** + +### Pattern 1: range filter + +- [ ] Add test cases covering range boundaries and at least one mid-range value +- [ ] If shared problems exist: add a test case with mixed contest_ids to confirm exclusion +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider using `parseContestRound()` range check +- [ ] `pnpm test:unit ` — **expect GREEN** + +### Pattern 3: composite + +- [ ] Confirm whether `prisma/contest_task_pairs.ts` needs new entries before writing tests +- [ ] Add test cases for each constituent contest_id, plus a mixed-source test +- [ ] If section splits: add one test per section +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider (filter by `classifyContest` equality; add section subclasses if needed) +- [ ] `pnpm test:unit ` — **expect GREEN** + +--- + +## Layer 5 — Group registration (TDD) + +- [ ] Update `contest_table_provider_groups.test.ts`: + - New group name string, `buttonLabel`, `ariaLabel` + - `getSize()` incremented to reflect the new provider count + - Add `getProvider(ContestType.XXX)` assertion + - Add import of new Provider class +- [ ] `pnpm test:unit contest_table_provider_groups.test.ts` — **expect RED** +- [ ] Update `contest_table_provider_groups.ts`: + - Add import of new Provider class + - Update group name string, `buttonLabel`, `ariaLabel` + - Add `new XXXProvider(ContestType.XXX)` to `addProviders()` +- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **expect GREEN** + +--- + +## Final verification + +- [ ] `pnpm test:unit` +- [ ] `pnpm check` +- [ ] `pnpm lint` + +Commit Layer 1–3 and Layer 4–5 as separate commits. diff --git a/docs/guides/claude-code.md b/docs/guides/claude-code.md index 36314f313..dbd203b32 100644 --- a/docs/guides/claude-code.md +++ b/docs/guides/claude-code.md @@ -65,12 +65,13 @@ paths: **本プロジェクトの skills(`.claude/skills/` および superpowers plugin):** -| スキル | 用途 | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `/writing-plans` | 新機能・追加実装の詳細計画を生成(2-5分単位のタスク分解)。superpowers plugin 提供 | -| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) | -| `/session-close` | セッション終了時のルーティン:テスト確認 → plan.md 更新 → rules 候補提示 → 肥大化チェック → 繰り返し指示検出 | -| `/dep-upgrade` | ライブラリのメジャーバージョンアップ分析:破壊的変更の整理・本プロジェクトへの影響・新機能提案 → plan.md 生成 → アップグレード実行 | +| スキル | 用途 | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/writing-plans` | 新機能・追加実装の詳細計画を生成(2-5分単位のタスク分解)。superpowers plugin 提供 | +| `/add-contest-table-provider` | 新しい ContestType と ContestTableProvider を TDD で 5 層実装(Prisma → 型 → ユーティリティ → Provider → グループ登録)。3 パターン対応。実装前に要件を確認する | +| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) | +| `/session-close` | セッション終了時のルーティン:テスト確認 → plan.md 更新 → rules 候補提示 → 肥大化チェック → 繰り返し指示検出 | +| `/dep-upgrade` | ライブラリのメジャーバージョンアップ分析:破壊的変更の整理・本プロジェクトへの影響・新機能提案 → plan.md 生成 → アップグレード実行 | **`/dep-upgrade` の使い方:** diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index 28886874c..cbf5dd60e 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -428,7 +428,7 @@ export const taskResultsForNewProvider: TaskResults = [ --- -## よくあるミス Top 4 +## よくあるミス Top 5 ### 1. **getDisplayConfig() での属性漏れ** @@ -504,6 +504,31 @@ describe('CustomProvider with unique config', () => { --- +### 5. **contestTypePriorities の JSDoc カテゴリ名を変更してしまう** + +**問題**: 新しい ContestType を挿入して数値範囲が変わったとき、既存の4カテゴリ名 +(`Educational` / `Contests for genius` / `Special contests` / `External platforms`)を +意図せず改名・分割・合体してしまい、歴史的経緯や分類上の意味が失われる。 + +**解決策**: **カテゴリ名は絶対に変更しない**。変えてよいのは括弧内の数値範囲だけ。 + +```typescript +// Before: [ContestType.TDPC, 5] ... [ContestType.PAST, 6] +// After inserting NDPC at 6: +// [ContestType.NDPC, 6], [ContestType.PAST, 7], ... + +// ✅ 数値範囲だけ更新 +// Educational contests (0–11, 17) +// Contests for genius (12–16) +// Special contests (18–20) +// External platforms (21–23) + +// ❌ カテゴリを改名・分割・合体しない +// Educational / DP contests (0–6) ← NG +``` + +--- + ## 実装完了後 ### ドキュメント更新チェックリスト From cc14e72a6388cd8d58b6fa7a4260187bb4465637 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 08:10:01 +0000 Subject: [PATCH 5/6] feat: add NDPC to ContestType enum and fix atcoder_account updatedAt default - Add NDPC (National Informatics Olympiad - Domestic Programming Contest) to ContestType - Fix atcoder_account.updatedAt to drop incorrect DEFAULT CURRENT_TIMESTAMP (managed by Prisma @updatedAt) Co-Authored-By: Claude Haiku 4.5 --- .../migration.sql | 4 ++++ .../20260510080034_add_ndpc_to_contest_type/migration.sql | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql create mode 100644 prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql diff --git a/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql b/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql new file mode 100644 index 000000000..fb8dca5be --- /dev/null +++ b/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql @@ -0,0 +1,4 @@ +-- @updatedAt is managed by Prisma ORM, not the DB layer. The original +-- split_atcoder_account migration incorrectly added DEFAULT CURRENT_TIMESTAMP +-- to updatedAt. This migration aligns the migration history with the actual DB state. +ALTER TABLE "atcoder_account" ALTER COLUMN "updatedAt" DROP DEFAULT; diff --git a/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql b/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql new file mode 100644 index 000000000..42b043535 --- /dev/null +++ b/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ContestType" ADD VALUE 'NDPC'; From c057e8a911fae7de88e5789c01598886d853b536 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 10 May 2026 08:11:34 +0000 Subject: [PATCH 6/6] chore: remove completed NDPC implementation plan Plan implementation is complete. Archive plan per AGENTS.md guidelines. Co-Authored-By: Claude Haiku 4.5 --- .../add-ndpc-contest-table-provider/plan.md | 442 ------------------ 1 file changed, 442 deletions(-) delete mode 100644 docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md diff --git a/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md b/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md deleted file mode 100644 index 1e17e810f..000000000 --- a/docs/dev-notes/2026-05-10/add-ndpc-contest-table-provider/plan.md +++ /dev/null @@ -1,442 +0,0 @@ -# Next DP Contest (NDPC) コンテストテーブル追加 実装計画 - -**Goal:** コンテストテーブルに Next DP Contest (NDPC) を追加し、EDPC・TDPC・FPS 24 グループに統合する。 - -**Architecture:** `ContestType.NDPC` を Prisma スキーマ → TypeScript 定数 → `classifyContest` / `contestTypePriorities` / `getContestNameLabel` → `NDPCProvider` → `dps` グループの順に追加する。TDD で各レイヤーを実装する。 - -**Tech Stack:** Prisma (PostgreSQL enum), TypeScript, Vitest - ---- - -## Context - -問題データ (`prisma/tasks.ts` 6349–6488 行) はすでに存在する (`contest_id: 'ndpc'`、問題 A–T の 20 問)。しかし `ContestType.NDPC` が未定義のため、コンテストテーブル上に表示されない。ユーザー指定の仕様: - -- 順番: `TDPC` の直後に配置 -- `dps` グループの buttonLabel: `EDPC・TDPC・NDPC・FPS 24` - -## 設計の根拠 - -- `NDPCProvider` は `TDPCProvider` と同じ Pattern 2 (Single Source) — `contest_id === 'ndpc'` による単一フィルター -- `contestTypePriorities` に NDPC=6 を割り当て、以後のエントリを全て1ずつ上げる (PAST=7, ACL_PRACTICE=8, ..., AOJ_JAG=23) - - 理由: 整数優先度の一貫性を保つ。浮動小数点は保守性を損なう。 -- `dp_providers.ts` に同居させることで DP 系プロバイダーをひとつのファイルに集約する (EDPC・TDPC と同じ理由) - -## 却下した代替案 - -- **独立ファイル `ndpc_provider.ts` に分離**: DP 系は `dp_providers.ts` に集約する既存慣習に反する。問題数の違いはプロバイダー実装に影響しない。 - ---- - -## 変更ファイル一覧 - -| ファイル | 操作 | -| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -| `prisma/schema.prisma` | `ContestType` enum に `NDPC` 追加 | -| `src/lib/types/contest.ts` | `ContestType` オブジェクトに `NDPC` 追加 | -| `src/lib/utils/contest.ts` | `classifyContest` / `contestTypePriorities` / `getContestNameLabel` に NDPC 追記 | -| `src/test/lib/utils/test_cases/contest_type.ts` | `ndpc` テストケース追加 | -| `src/test/lib/utils/test_cases/contest_name_labels.ts` | `ndpc` テストケース追加 | -| `src/test/lib/utils/contest.test.ts` | 3 つの `describe` ブロック追加 | -| `src/features/tasks/utils/contest-table/dp_providers.ts` | `NDPCProvider` クラス追加 | -| `src/features/tasks/utils/contest-table/dp_providers.test.ts` | NDPC をパラメータ化テストに追加 | -| `src/features/tasks/utils/contest-table/contest_table_provider_groups.ts` | `dps` プリセット更新 | -| `src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts` | `dps` テスト更新 | - ---- - -## Task 1: Prisma スキーマ + マイグレーション - -**Files:** - -- Modify: `prisma/schema.prisma:307` - -- [ ] **Step 1: schema.prisma を編集** - -`TDPC // Typical DP Contest` の直後に追加: - -```prisma -NDPC // Next DP Contest -``` - -- [ ] **Step 2: マイグレーション実行** - -```bash -pnpm exec prisma migrate dev --name add_ndpc_to_contest_type -``` - -Expected: `Your database is now in sync with your schema.` のようなメッセージ。`@prisma/client` が再生成され `ContestTypeOrigin` に `NDPC` が含まれる。 - -- [ ] **Step 3: 型チェック** - -```bash -pnpm check -``` - -Expected: `src/lib/types/contest.ts` で型エラー(`NDPC` がオブジェクトに無い)。これは Task 2 で修正する。 - ---- - -## Task 2: TypeScript ContestType 定数更新 - -**Files:** - -- Modify: `src/lib/types/contest.ts:37` - -- [ ] **Step 1: NDPC を追加** - -```typescript -NDPC: 'NDPC', // Next DP Contest -``` - -`TDPC: 'TDPC', // Typical DP Contest` の直後に挿入。 - -- [ ] **Step 2: 型チェック** - -```bash -pnpm check -``` - -Expected: エラーなし(または NDPC 未使用の警告のみ)。 - ---- - -## Task 3: classifyContest / contestTypePriorities / getContestNameLabel 更新 (TDD) - -**Files:** - -- Modify: `src/test/lib/utils/test_cases/contest_type.ts` -- Modify: `src/test/lib/utils/test_cases/contest_name_labels.ts` -- Modify: `src/test/lib/utils/contest.test.ts` -- Modify: `src/lib/utils/contest.ts` - -- [ ] **Step 1: テストケースデータを追加 (contest_type.ts)** - -`tdpc` エクスポートの直後に追加: - -```typescript -export const ndpc = [ - createTestCaseForContestType('NDPC')({ - contestId: 'ndpc', - expected: ContestType.NDPC, - }), -]; -``` - -- [ ] **Step 2: テストケースデータを追加 (contest_name_labels.ts)** - -`tdpc` エクスポートの直後に追加: - -```typescript -export const ndpc = [ - createTestCaseForContestNameLabel('NDPC')({ - contestId: 'ndpc', - expected: 'NDPC', - }), -]; -``` - -- [ ] **Step 3: describe ブロックを追加 (contest.test.ts)** - -`classifyContest`・`getContestPriority`・`getContestNameLabel` の各 `when contest_id is tdpc` describe の直後に、それぞれ以下を追加: - -```typescript -describe('when contest_id is ndpc', () => { - TestCasesForContestType.ndpc.forEach(({ name, value }) => { - runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { - expect(classifyContest(contestId)).toEqual(expected); - }); - }); -}); -``` - -```typescript -describe('when contest_id is ndpc', () => { - TestCasesForContestType.ndpc.forEach(({ name, value }) => { - runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { - expect(getContestPriority(contestId)).toEqual(contestTypePriorities.get(expected)); - }); - }); -}); -``` - -```typescript -describe('when contest_id is ndpc', () => { - TestCasesForContestNameLabel.ndpc.forEach(({ name, value }) => { - runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { - expect(getContestNameLabel(contestId)).toEqual(expected); - }); - }); -}); -``` - -- [ ] **Step 4: テスト失敗を確認** - -```bash -pnpm test:unit src/test/lib/utils/contest.test.ts -``` - -Expected: NDPC 関連テストが FAIL。 - -- [ ] **Step 5: contest.ts を実装** - -`classifyContest` — `if (contest_id === 'tdpc')` ブロックの直後に追加: - -```typescript -if (contest_id === 'ndpc') { - return ContestType.NDPC; -} -``` - -`contestTypePriorities` — 全体の優先度を調整: - -- `[ContestType.NDPC, 6]` を TDPC の直後に追加 -- 以後のエントリを全て 1 ずつ上げる (PAST: 6→7, ACL_PRACTICE: 7→8, ..., AOJ_JAG: 22→23) - -新しい優先度マップ: - -```typescript -[ContestType.ABS, 0], -[ContestType.ABC, 1], -[ContestType.APG4B, 2], -[ContestType.TYPICAL90, 3], -[ContestType.EDPC, 4], -[ContestType.TDPC, 5], -[ContestType.NDPC, 6], // <- 新規 -[ContestType.PAST, 7], // <- 6→7 -[ContestType.ACL_PRACTICE, 8], // <- 7→8 -[ContestType.JOI, 9], // <- 8→9 -[ContestType.TESSOKU_BOOK, 10], // <- 9→10 -[ContestType.MATH_AND_ALGORITHM, 11], // <- 10→11 -[ContestType.ARC, 12], // <- 11→12 -[ContestType.AGC, 13], // <- 12→13 -[ContestType.ABC_LIKE, 14], // <- 13→14 -[ContestType.ARC_LIKE, 15], // <- 14→15 -[ContestType.AGC_LIKE, 16], // <- 15→16 -[ContestType.AWC, 17], // <- 16→17 -[ContestType.UNIVERSITY, 18], // <- 17→18 -[ContestType.FPS_24, 19], // <- 18→19 -[ContestType.OTHERS, 20], // <- 19→20 -[ContestType.AOJ_COURSES, 21], // <- 20→21 -[ContestType.AOJ_PCK, 22], // <- 21→22 -[ContestType.AOJ_JAG, 23], // <- 22→23 -``` - -`getContestNameLabel` — `if (contestId === 'tdpc') { return 'TDPC'; }` の直後に追加: - -```typescript -if (contestId === 'ndpc') { - return 'NDPC'; -} -``` - -- [ ] **Step 6: テスト通過を確認** - -```bash -pnpm test:unit src/test/lib/utils/contest.test.ts -``` - -Expected: すべて PASS。 - -- [ ] **Step 7: コミット** - -```bash -git add prisma/schema.prisma prisma/migrations/ src/lib/types/contest.ts src/lib/utils/contest.ts src/test/lib/utils/test_cases/contest_type.ts src/test/lib/utils/test_cases/contest_name_labels.ts src/test/lib/utils/contest.test.ts -git commit -m "feat: add NDPC ContestType with classifyContest and name label support" -``` - ---- - -## Task 4: NDPCProvider 実装 (TDD) - -**Files:** - -- Modify: `src/features/tasks/utils/contest-table/dp_providers.test.ts` -- Modify: `src/features/tasks/utils/contest-table/dp_providers.ts` - -- [ ] **Step 1: テストを追加 (dp_providers.test.ts)** - -`describe.each` の配列に NDPC エントリを追加: - -```typescript -{ - providerClass: NDPCProvider, - contestType: ContestType.NDPC, - contestId: 'ndpc', - title: 'Next DP Contest', - abbreviationName: 'ndpc', - label: 'NDPC provider', -}, -``` - -import 行も更新: - -```typescript -import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; -``` - -- [ ] **Step 2: テスト失敗を確認** - -```bash -pnpm test:unit src/features/tasks/utils/contest-table/dp_providers.test.ts -``` - -Expected: `NDPCProvider` が未定義で FAIL。 - -- [ ] **Step 3: NDPCProvider を実装 (dp_providers.ts)** - -`TDPCProvider` クラスの直後に追加: - -```typescript -export class NDPCProvider extends ContestTableProviderBase { - protected setFilterCondition(): (taskResult: TaskResult) => boolean { - return (taskResult: TaskResult) => { - if (classifyContest(taskResult.contest_id) !== this.contestType) { - return false; - } - - return taskResult.contest_id === 'ndpc'; - }; - } - - getMetadata(): ContestTableMetaData { - return { - title: 'Next DP Contest', - abbreviationName: 'ndpc', - }; - } - - getDisplayConfig(): ContestTableDisplayConfig { - return { - isShownHeader: false, - isShownRoundLabel: false, - roundLabelWidth: '', - tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - isShownTaskIndex: true, - }; - } - - getContestRoundLabel(_contestId: string): string { - return ''; - } -} -``` - -- [ ] **Step 4: テスト通過を確認** - -```bash -pnpm test:unit src/features/tasks/utils/contest-table/dp_providers.test.ts -``` - -Expected: すべて PASS。 - ---- - -## Task 5: dps グループに NDPCProvider を登録 - -**Files:** - -- Modify: `src/features/tasks/utils/contest-table/contest_table_provider_groups.ts:26,187-195` -- Modify: `src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts` - -- [ ] **Step 1: テストを更新 (contest_table_provider_groups.test.ts)** - -`expects to create DPs preset correctly` テストを更新: - -```typescript -test('expects to create DPs preset correctly', () => { - const group = prepareContestProviderPresets().dps(); - - expect(group.getGroupName()).toBe('EDPC・TDPC・NDPC・FPS 24'); - expect(group.getMetadata()).toEqual({ - buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', - ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', - }); - expect(group.getSize()).toBe(4); - expect(group.getProvider(ContestType.EDPC)).toBeInstanceOf(EDPCProvider); - expect(group.getProvider(ContestType.TDPC)).toBeInstanceOf(TDPCProvider); - expect(group.getProvider(ContestType.NDPC)).toBeInstanceOf(NDPCProvider); - expect(group.getProvider(ContestType.FPS_24)).toBeInstanceOf(FPS24Provider); -}); -``` - -import 行に `NDPCProvider` を追加。 - -- [ ] **Step 2: テスト失敗を確認** - -```bash -pnpm test:unit src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts -``` - -Expected: グループ名・サイズ・NDPC provider のアサーションが FAIL。 - -- [ ] **Step 3: contest_table_provider_groups.ts を更新** - -import 行を更新: - -```typescript -import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; -``` - -`dps` プリセットを更新: - -```typescript -dps: () => - new ContestTableProviderGroup(`EDPC・TDPC・NDPC・FPS 24`, { - buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', - ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', - }).addProviders( - new EDPCProvider(ContestType.EDPC), - new TDPCProvider(ContestType.TDPC), - new NDPCProvider(ContestType.NDPC), - new FPS24Provider(ContestType.FPS_24), - ), -``` - -- [ ] **Step 4: テスト通過を確認** - -```bash -pnpm test:unit src/features/tasks/utils/contest-table/ -``` - -Expected: すべて PASS。 - -- [ ] **Step 5: 全テストを実行** - -```bash -pnpm test:unit -``` - -Expected: すべて PASS。 - -- [ ] **Step 6: 型チェック** - -```bash -pnpm check -``` - -Expected: エラーなし。 - -- [ ] **Step 7: コミット** - -```bash -git add src/features/tasks/utils/contest-table/dp_providers.ts src/features/tasks/utils/contest-table/dp_providers.test.ts src/features/tasks/utils/contest-table/contest_table_provider_groups.ts src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts -git commit -m "feat: add NDPCProvider and register in dps contest group" -``` - ---- - -## Verification - -```bash -# 全テスト -pnpm test:unit - -# 型チェック -pnpm check - -# lint -pnpm lint -``` - -期待動作: `pnpm dev` 起動後、コンテストテーブルのフィルターに `EDPC・TDPC・NDPC・FPS 24` ボタンが表示され、NDPC の 20 問 (A–T) が表示される。