From 3569e9af723f4daa84e632ce365bde1c953b6e6b Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:06:27 +0000 Subject: [PATCH 1/7] docs: Create plan (#3223) --- .../add-aoj-course-to-contests/plan.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md diff --git a/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md new file mode 100644 index 000000000..bef63bab1 --- /dev/null +++ b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md @@ -0,0 +1,83 @@ +# AOJ コースへの DSL・CGL・NTL 追加 (Issue #3223) + +## 背景 + +[Issue #3223](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3223) で、AOJ (Aizu Online Judge) の以下の3コースを AtCoderNoviSteps に追加することが要求されています。 + +- **DSL** (データ構造) — 18 問 +- **CGL** (計算幾何学) — 25 問 +- **NTL** (整数論) — 11 問 + +現在 `AOJ_COURSES` には ITP1・ALDS1・ITP2・DPL・GRL の5件が登録されています。新コースも同じパターンで追加するだけで、アーキテクチャ上の変更は不要です。 + +## 変更対象ファイル + +### 1. `src/lib/utils/contest.ts` — `AOJ_COURSES` に3件追加 + +```typescript +export const AOJ_COURSES: ContestPrefix = { + ITP1: 'プログラミング入門', + ALDS1: 'アルゴリズムとデータ構造入門', + ITP2: 'プログラミング応用', + DPL: '組み合わせ最適化', + GRL: 'グラフ', + DSL: 'データ構造', // 追加 + CGL: '計算幾何学', // 追加 + NTL: '整数論', // 追加 +} as const; +``` + +`AOJ_COURSES` を参照している以下の関数・変数はデータ駆動なので変更不要: + +- `getPrefixForAojCourses()` / `aojCoursePrefixes` Set +- `classifyContest()` +- `getContestNameLabel()` +- `isAojContest()` + +### 2. `src/test/lib/utils/test_cases/contest_type.ts` — `aojCoursesData` に3件追加 + +```typescript +const aojCoursesData = [ + // 既存エントリ... + { name: 'AOJ Courses, DSL', contestId: 'DSL' }, + { name: 'AOJ Courses, CGL', contestId: 'CGL' }, + { name: 'AOJ Courses, NTL', contestId: 'NTL' }, +]; +``` + +### 3. `src/test/lib/utils/test_cases/contest_name_and_task_index.ts` — `AOJ_COURSES_TEST_DATA` に3件追加 + +```typescript +const AOJ_COURSES_TEST_DATA = { + // 既存エントリ... + DSL: { + contestId: 'DSL', + tasks: ['1_A', '2_H'], + }, + CGL: { + contestId: 'CGL', + tasks: ['1_A', '7_I'], + }, + NTL: { + contestId: 'NTL', + tasks: ['1_A', '2_F'], + }, +}; +``` + +## 変更不要なもの + +| ファイル | 理由 | +| ------------------------------------------- | ------------------------------------------------------ | +| `src/lib/clients/aizu_online_judge.ts` | AOJ API から動的にコースを取得するため | +| `prisma/schema.prisma` | `ContestType.AOJ_COURSES` が既にすべてのコースをカバー | +| DB マイグレーション | スキーマ変更なし | +| `src/test/lib/utils/test_cases/task_url.ts` | AOJ_COURSES 参照なし、変更不要 | + +## 確認方法 + +```bash +pnpm test:unit # テストが通ること +pnpm check # TypeScript 型チェック +pnpm lint # ESLint チェック +``` From fb0051e3c14b373cef61e4bfbd76c08b8c491804 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:41:03 +0000 Subject: [PATCH 2/7] feat: Add DSL, CGL and NTL to AOJ courses (#3223) --- src/lib/utils/contest.ts | 3 +++ .../utils/test_cases/contest_name_and_task_index.ts | 12 ++++++++++++ src/test/lib/utils/test_cases/contest_type.ts | 3 +++ 3 files changed, 18 insertions(+) diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 9df44b5b1..74319b8e1 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -225,6 +225,9 @@ export const AOJ_COURSES: ContestPrefix = { ITP2: 'プログラミング応用', DPL: '組み合わせ最適化', GRL: 'グラフ', + DSL: 'データ構造', + CGL: '計算幾何学', + NTL: '整数論', } as const; export function getPrefixForAojCourses() { diff --git a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts index a07e6da9f..4c3fb3489 100644 --- a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts +++ b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts @@ -615,6 +615,18 @@ const AOJ_COURSES_TEST_DATA = { contestId: 'GRL', tasks: ['1_A', '1_C', '6_B', '7_A'], }, + DSL: { + contestId: 'DSL', + tasks: ['1_A', '2_H'], + }, + CGL: { + contestId: 'CGL', + tasks: ['1_A', '7_I'], + }, + NTL: { + contestId: 'NTL', + tasks: ['1_A', '2_F'], + }, }; const generateAojCoursesTestCases = ( diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 5299c13b9..d5b628350 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -508,6 +508,9 @@ const aojCoursesData = [ { name: 'AOJ Courses, ITP2', contestId: 'ITP2' }, { name: 'AOJ Courses, DPL', contestId: 'DPL' }, { name: 'AOJ Courses, GRL', contestId: 'GRL' }, + { name: 'AOJ Courses, DSL', contestId: 'DSL' }, + { name: 'AOJ Courses, CGL', contestId: 'CGL' }, + { name: 'AOJ Courses, NTL', contestId: 'NTL' }, ]; export const aojCourses = aojCoursesData.map(({ name, contestId }) => From 66d6267f6a15b6d1d1351e2e56704a05e119f114 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:41:22 +0000 Subject: [PATCH 3/7] docs: Add note to plan (#3223) --- .../add-aoj-course-to-contests/plan.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md index bef63bab1..00f4fd0f9 100644 --- a/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md +++ b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md @@ -81,3 +81,21 @@ pnpm test:unit # テストが通ること pnpm check # TypeScript 型チェック pnpm lint # ESLint チェック ``` + +## 注意事項 + +### AOJ API に存在しない問題 + タイトルの重複による id 衝突 + +AOJ の `/problems?size=N` エンドポイントは、実際には存在しない(ページが not found になる)問題を返すことがある。例えば `CGL_4_D` は AOJ 上では存在しない + API レスポンスに `CGL_2_B` と `CGL_7_A` と同じタイトルの場合がある。 + +現在のタスク `id` 生成ロジックは `sha256(contest_id + task.title)` であるため、同一コース内でタイトルが重複すると `Task.id` の一意制約違反(P2002)が発生する。 + +- **影響**: CGL コースのインポート時に `PrismaClientKnownRequestError: Unique constraint failed on the fields: (\`id\`)` が発生する +- **根本原因**: `sha256(contest_id + title)` は title の重複に弱い。`task.id`(API のユニーク識別子)を使うべき +- **暫定対処**: 該当コースの存在する問題のみインポート + +## 実装後の教訓 + +- `AOJ_COURSES` はデータ駆動設計になっており、新コース追加は定数オブジェクトに1行足すだけで、呼び出し側コードの変更は不要だった。 +- テストデータも同様に、既存パターンをそのまま踏襲して追記するだけで済む。 +- プランの「変更不要なもの」セクションを事前に確認しておくと、余計な調査を省ける。 From 1ca400688778e46aa57a11231f76a8ff71bb2a95 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:43:28 +0000 Subject: [PATCH 4/7] docs: Revised plan (#3223) --- .../add-aoj-course-to-contests/plan.md | 98 +------------------ 1 file changed, 2 insertions(+), 96 deletions(-) diff --git a/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md index 00f4fd0f9..7babb2e47 100644 --- a/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md +++ b/docs/dev-notes/2026-03-01/add-aoj-course-to-contests/plan.md @@ -1,101 +1,7 @@ # AOJ コースへの DSL・CGL・NTL 追加 (Issue #3223) -## 背景 - -[Issue #3223](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3223) で、AOJ (Aizu Online Judge) の以下の3コースを AtCoderNoviSteps に追加することが要求されています。 - -- **DSL** (データ構造) — 18 問 -- **CGL** (計算幾何学) — 25 問 -- **NTL** (整数論) — 11 問 - -現在 `AOJ_COURSES` には ITP1・ALDS1・ITP2・DPL・GRL の5件が登録されています。新コースも同じパターンで追加するだけで、アーキテクチャ上の変更は不要です。 - -## 変更対象ファイル - -### 1. `src/lib/utils/contest.ts` — `AOJ_COURSES` に3件追加 - -```typescript -export const AOJ_COURSES: ContestPrefix = { - ITP1: 'プログラミング入門', - ALDS1: 'アルゴリズムとデータ構造入門', - ITP2: 'プログラミング応用', - DPL: '組み合わせ最適化', - GRL: 'グラフ', - DSL: 'データ構造', // 追加 - CGL: '計算幾何学', // 追加 - NTL: '整数論', // 追加 -} as const; -``` - -`AOJ_COURSES` を参照している以下の関数・変数はデータ駆動なので変更不要: - -- `getPrefixForAojCourses()` / `aojCoursePrefixes` Set -- `classifyContest()` -- `getContestNameLabel()` -- `isAojContest()` - -### 2. `src/test/lib/utils/test_cases/contest_type.ts` — `aojCoursesData` に3件追加 - -```typescript -const aojCoursesData = [ - // 既存エントリ... - { name: 'AOJ Courses, DSL', contestId: 'DSL' }, - { name: 'AOJ Courses, CGL', contestId: 'CGL' }, - { name: 'AOJ Courses, NTL', contestId: 'NTL' }, -]; -``` - -### 3. `src/test/lib/utils/test_cases/contest_name_and_task_index.ts` — `AOJ_COURSES_TEST_DATA` に3件追加 - -```typescript -const AOJ_COURSES_TEST_DATA = { - // 既存エントリ... - DSL: { - contestId: 'DSL', - tasks: ['1_A', '2_H'], - }, - CGL: { - contestId: 'CGL', - tasks: ['1_A', '7_I'], - }, - NTL: { - contestId: 'NTL', - tasks: ['1_A', '2_F'], - }, -}; -``` - -## 変更不要なもの - -| ファイル | 理由 | -| ------------------------------------------- | ------------------------------------------------------ | -| `src/lib/clients/aizu_online_judge.ts` | AOJ API から動的にコースを取得するため | -| `prisma/schema.prisma` | `ContestType.AOJ_COURSES` が既にすべてのコースをカバー | -| DB マイグレーション | スキーマ変更なし | -| `src/test/lib/utils/test_cases/task_url.ts` | AOJ_COURSES 参照なし、変更不要 | - -## 確認方法 - -```bash -pnpm test:unit # テストが通ること -pnpm check # TypeScript 型チェック -pnpm lint # ESLint チェック -``` +[Issue #3223](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3223) で、AOJ の DSL (18問)・CGL (25問)・NTL (11問) を追加。`AOJ_COURSES` に3件追記するだけで、アーキテクチャ上の変更は不要。`AOJ_COURSES` を参照する関数はデータ駆動のため変更不要。 ## 注意事項 -### AOJ API に存在しない問題 + タイトルの重複による id 衝突 - -AOJ の `/problems?size=N` エンドポイントは、実際には存在しない(ページが not found になる)問題を返すことがある。例えば `CGL_4_D` は AOJ 上では存在しない + API レスポンスに `CGL_2_B` と `CGL_7_A` と同じタイトルの場合がある。 - -現在のタスク `id` 生成ロジックは `sha256(contest_id + task.title)` であるため、同一コース内でタイトルが重複すると `Task.id` の一意制約違反(P2002)が発生する。 - -- **影響**: CGL コースのインポート時に `PrismaClientKnownRequestError: Unique constraint failed on the fields: (\`id\`)` が発生する -- **根本原因**: `sha256(contest_id + title)` は title の重複に弱い。`task.id`(API のユニーク識別子)を使うべき -- **暫定対処**: 該当コースの存在する問題のみインポート - -## 実装後の教訓 - -- `AOJ_COURSES` はデータ駆動設計になっており、新コース追加は定数オブジェクトに1行足すだけで、呼び出し側コードの変更は不要だった。 -- テストデータも同様に、既存パターンをそのまま踏襲して追記するだけで済む。 -- プランの「変更不要なもの」セクションを事前に確認しておくと、余計な調査を省ける。 +AOJ の `/problems?size=N` は存在しない問題を返すことがある。現在の `id` 生成ロジックは `sha256(contest_id + task.title)` のため、タイトル重複時に `Task.id` 一意制約違反 (P2002) が発生する(CGL で確認)。根本対処は `task.id`(API のユニーク識別子)を使うこと。暫定対処として存在する問題のみインポート。 From aa363557ee820e74fb4380f1a5f45ca3011cfce9 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:51:52 +0000 Subject: [PATCH 5/7] test: Add test cases (#3223) --- src/test/lib/utils/test_cases/task_url.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/lib/utils/test_cases/task_url.ts b/src/test/lib/utils/test_cases/task_url.ts index a67f2d1c3..50e9d20b7 100644 --- a/src/test/lib/utils/test_cases/task_url.ts +++ b/src/test/lib/utils/test_cases/task_url.ts @@ -104,6 +104,18 @@ const courses = [ contestId: 'GRL', tasks: ['1_A', '1_C', '6_B', '7_A'], }, + { + contestId: 'DSL', + tasks: ['1_A', '1_B', '2_I', '5_B'], + }, + { + contestId: 'CGL', + tasks: ['1_A', '1_C', '7_I'], + }, + { + contestId: 'NTL', + tasks: ['1_a', '1_E', '2_F'], + }, ]; export const aojCourses = courses.flatMap((course) => From b1aaed9adbcd4ca06815f6d452cf214b47a90ad9 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:52:35 +0000 Subject: [PATCH 6/7] chore: Fix typo (#3223) --- src/test/lib/utils/test_cases/task_url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/lib/utils/test_cases/task_url.ts b/src/test/lib/utils/test_cases/task_url.ts index 50e9d20b7..2cf596094 100644 --- a/src/test/lib/utils/test_cases/task_url.ts +++ b/src/test/lib/utils/test_cases/task_url.ts @@ -114,7 +114,7 @@ const courses = [ }, { contestId: 'NTL', - tasks: ['1_a', '1_E', '2_F'], + tasks: ['1_A', '1_E', '2_F'], }, ]; From 30475a283994aeee4b6cdb41073409838f55cc65 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Mar 2026 13:59:45 +0000 Subject: [PATCH 7/7] chore: Update JSDoc comment (#3223) --- src/test/lib/utils/test_cases/contest_name_and_task_index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts index 4c3fb3489..ef9e64046 100644 --- a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts +++ b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts @@ -593,6 +593,10 @@ export const agc = generateAgcTestCases( * - ALDS1: Algorithms and Data Structures I * - ITP2: Introduction to Programming II * - DPL: Discrete Optimization Problems + * - GRL: Graph Algorithms + * - DSL: Data Set and Queries + * - CGL: Computational Geometry + * - NTL: Number Theory */ const AOJ_COURSES_TEST_DATA = { ITP1: {