From 08c2c226e4f9b506c558f9594d7f8feabaf0658b Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 02:08:46 +0000 Subject: [PATCH 1/7] docs: fold the PR2 plan into the decision summary Co-Authored-By: Claude Sonnet 5.5 --- .../task-title-index-change-impact/done.md | 49 ++++- .../investigation.md | 6 +- .../plan-pr2.md | 203 ------------------ .../task-title-index-change-impact/plan.md | 35 +-- 4 files changed, 66 insertions(+), 227 deletions(-) delete mode 100644 docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.md diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/done.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/done.md index a439422e2..5fd6aa615 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/done.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/done.md @@ -1,8 +1,8 @@ -# 完了分(PR0 / PR1 / PR1.5)— 意思決定の要約 +# 完了分(PR0 / PR1 / PR1.5 / PR2)— 意思決定の要約 -**状態**: PR0 完了(#4212)・PR1 完了(#4217)・PR1.5 完了(#4219)。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 +**状態**: PR0 完了(#4212)・PR1 完了(#4217)・PR1.5 完了(#4219)・PR2 は staging に反映(#4220、本番は未反映)。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 -コードと git の履歴から読み取れない判断と事実だけを残す。変更したファイルや関数は #4212 / #4217 と `471ad20c` の差分を見る。PR1.5 は [末尾](#pr15--aoj-api-の移転に追従する4028)。 +コードと git の履歴から読み取れない判断と事実だけを残す。変更したファイルや関数は #4212 / #4217 / #4219 / #4220 の差分を見る。PR1.5 は [こちら](#pr15--aoj-api-の移転に追従する4028)、PR2 は [こちら](#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030)。 ## 何をしたか @@ -76,4 +76,45 @@ PR2 以降の計画には含めていない。 ### 残っている確認 - ~~コンテナを再ビルドした後、ローカルの管理画面で AOJ から取得できること~~ — 2026-10-24 にメンテナが確認した -- ~~staging(Vercel)での取り込み~~ — 2026-10-24 にメンテナが JAG REGIONAL を取り込み、16 / 158 になった([plan-pr2.md](./plan-pr2.md#範囲))。こちらの取得ツールが新しいホストに `ECONNREFUSED` で拒まれたことから送信元の制限を心配していたが、Vercel からは問題なく取得できた +- ~~staging(Vercel)での取り込み~~ — 2026-10-24 にメンテナが JAG REGIONAL を取り込み、16 / 158 になった([PR2](#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030) の前提)。こちらの取得ツールが新しいホストに `ECONNREFUSED` で拒まれたことから送信元の制限を心配していたが、Vercel からは問題なく取得できた + +--- + +## PR2 — JAG 合宿系と JAG 模擬地区を問題記号にする(#4030) + +### 何をしたか + +- JAG 合宿系(夏 / 冬 / 春、50 コンテスト / 467 問)と JAG 模擬地区(16 / 158)の `task_table_index` を、問題 ID から問題記号にした。合宿系の title から、前に付いていた `<問題 ID>. ` を外した。移行 SQL は `prisma/migrations/20261025000719_update_aoj_jag_task_table_index/` +- seed に、本番だけにあった模擬地区 158 問を足した(grade は本番のまま。3355 の title は trim) +- 管理画面の取り込みで、JAG は全 round(模擬国内 / 模擬地区)で記号を付けるようにした +- 記号の対応表は、メンテナのスプレッドシートを書き写した照合スクリプトで作った(`git show c605d3bd:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_jag_problem_letters.ts`) + +### 決定と理由 + +| 決定 | 理由 | +| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 基本ルールで振り、8 コンテストはコンテストの全行を上書きする(45 行) | 8 コンテストは先頭や途中の問題が AOJ に無く、基本ルールでは記号がずれる。正しい記号は、メンテナのスプレッドシートの位置(欠けた問題を飛ばした位置)で決まる。全行を上書きするのは、1 つのコンテストに 2 つの規則を混ぜないため | +| 各ステップの更新件数を DO ブロックで検査し、想定外なら RAISE する | UPDATE は対象が 0 件でもエラーにならない。想定値は、空の DB(`prisma migrate reset`)の 0、模擬地区の無い開発 DB の 467 / 32 / 467、staging・本番の 625 / 45 / 467 | +| 件数は固定値のまま。模擬地区が一部だけの DB では RAISE し、エラー文で「全部取り込むか、全部消す」と案内する | 本番で範囲の誤りを止める力を保つため。模擬地区だけ実際の件数に合わせる案は、範囲の誤りを見逃しやすくなるので採らなかった | +| 順位はコンテストの全行で付け、更新は問題 ID のままの行だけにする | 後から記号付きで入る行(PR2.1)が先にあっても触らないため。順位を付ける側でも絞ると、記号の行と問題 ID の行が混ざったコンテストで A から振り直してしまう(Codex の指摘) | +| title から外すのは「自分の `task_id` + `. `」で始まるものだけ | `^[0-9]+\. ` で外すと、番号で始まる本来の title まで削りうる | +| 範囲は `contest_id` の接頭辞(`^JAG(Summer\|Winter\|Spring\|Regional)`)で絞る | `AOJ_JAG` には PR1 で記号にした模擬国内も含まれる | +| 取り込みでは補正しない(M14) | 8 コンテストは本番・staging・seed のすべてにあり、取り込みは既存の問題を skip する。Codex も「取り込みで補正すべき」と指摘したが、同じ理由で採らなかった | +| seed の検査は、補正した 8 コンテストを literal で、title を「問題名(`name`)と同じ」で見る | 補正の正しさは基本ルールの関数では検査できない。title は「問題 ID が前に付いていない」だけだと、空や別の問題の title にすり替わっても通る(Codex の指摘) | +| 前後に空白のある title(本番 46 件、3355 の末尾のタブを含む)は直さない | 表示では空白が潰れ、完全一致で比べるコードも無い。取り込みが trim するので再発もしない([plan.md](./plan.md#スコープ外)) | + +### 検証で分かったこと + +- **staging の準備**: staging には当初、模擬地区が 2 コンテスト / 16 問しか無く、件数チェックで RAISE するところだった。PR1.5 の後に staging で取り込み、PR を開く前に本番と同じ状態(模擬地区 16 / 158、合宿系 50 / 467、index はすべて問題 ID、title の問題 ID は合宿系だけ)であることを確かめた +- **seed の模擬地区**: 本番から取り出した 158 問は、AOJ の fixture(2026-05 記録)と問題 ID・コンテスト・名前がすべて一致した +- **照合**: スプレッドシートの書き写し(66 コンテスト)と 625 問の title が、記号の位置ですべて一致した。補正は 8 コンテスト / 45 行(事前の目視どおり)。わざと誤りを入れたコピー 5 種類(欠けた記号を外す、順番の入れ替え、途中の title の誤り、欠けた数の不足、切れた title の前半の誤り)はすべて検出された +- **ローカルでの移行**: 管理画面で模擬地区を取り込み、本番と同じ状態にしてから確かめた。`SELECT` のプレビューは 625 行すべてで照合の結果と一致し、適用後もプレビューと 625 / 625 で一致した。範囲外の AOJ(PCK、大学系、PR1 の範囲)は変わらない。`EXPLAIN` は CTE 側の Seq Scan 1 回と、`task_task_id_key` の Index Scan を使う Nested Loop +- **Codex の指摘の再現**: 記号の行と問題 ID の行が混ざったコンテストを読み取り専用で再現すると、直す前の SQL は A から振り直し、直した後は B, C, D と正しく振った。ローカルは移行前のスナップショットに戻し、直した移行を適用し直した(625 / 625) +- **テストの検出力**: 6 つの変異をすべて検出した(JAG の記号を PRELIM だけ / ICPC だけにする、補正した記号を基本ルールに戻す、基本ルールのコンテストで A・B を入れ替える、title に問題 ID を付け直す、模擬地区の記号を問題 ID に戻す)。title のテストは、空の title・別の問題の title・問題 ID の付け直しも検出する +- **Codex の他の指摘**: 「`task_id` だけで結合している」は、`task_id` が schema で `@unique` なので該当しない + +### 残っている確認 + +- ~~staging~~ — #4220 で反映した(2026-10-25)。移行の後に Vercel のデプロイが走るので、件数チェックも通っている +- 本番(`main` への昇格後)で、移行が通ること +- 問題一覧の出典欄の表示(例: `JAG 夏合宿 2024 Day3 - H(AOJ)`) diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/investigation.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/investigation.md index 6ba7aa8b6..6e4e18bdf 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/investigation.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/investigation.md @@ -17,7 +17,7 @@ PR0 / PR1 だけに関わる調査(出典欄の呼び出し元、テストの # 1. PR2 の照合(2026-10-24) -メンテナのスプレッドシート(スクリーンショット 16 枚、行 2〜1025。上が新しい)と、本番の問題一覧の JAG 模擬地区(スクリーンショット 10 枚、158 問)を目視で突き合わせた。**目視による暫定の結果**であり、確定は [plan-pr2.md](./plan-pr2.md#2-2-照合) のスクリプトで行う。 +メンテナのスプレッドシート(スクリーンショット 16 枚、行 2〜1025。上が新しい)と、本番の問題一覧の JAG 模擬地区(スクリーンショット 10 枚、158 問)を目視で突き合わせた。目視の結果で、PR2 の照合スクリプト(書き写したスプレッドシートとの機械的な照合)でも同じ結果になった([done.md](./done.md#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030))。 スプレッドシートで折りたたまれた行(40–50 ほか 19 か所)は JAG 模擬国内だけで、合宿系・模擬地区は含まない(メンテナに確認済み)。 @@ -228,7 +228,7 @@ AtCoder 側(`AOJ_*` 以外)で title が `${task_table_index}. ` で始ま ## PR2 の範囲の本番と staging(2026-10-24) -本番と staging で範囲の状態を集計した結果(SQL は [plan-pr2.md](./plan-pr2.md#確かめ方))。 +本番と staging で範囲の状態を集計した結果(SQL は `git show 7c68cef6:docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.md` の「確かめ方」)。 | 範囲 | 本番 | staging | index が問題 ID のまま | title が問題 ID 付き | | -------- | -------- | ------------------------------- | ---------------------- | -------------------- | @@ -238,7 +238,7 @@ AtCoder 側(`AOJ_*` 以外)で title が `${task_table_index}. ` で始ま | Winter | 10 / 87 | 10 / 87 | 全行 | 全行 | - staging には当初、模擬地区が 2 コンテスト / 16 問しか無かった。PR1.5(#4219)の後に staging で取り込み、本番と同じ 16 / 158 にそろえた(2026-10-24) -- 本番の模擬地区 158 問([plan-pr2.md](./plan-pr2.md#2-1-seed-に模擬地区を足す) の SQL で取り出したもの)は、AOJ の一覧のスクリーンショットと問題名・問題 ID がすべて一致した。grade が付いているのは 6 問 +- 本番の模擬地区 158 問(本番から取り出したもの)は、AOJ の一覧のスクリーンショットと問題名・問題 ID がすべて一致した。grade が付いているのは 6 問 - 本番の 3355 の title の末尾にタブがある(`Most distant point from the stations\t`)。前後に空白のある title はほかにもある(下記) ## 前後に空白のある title diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.md deleted file mode 100644 index afa3942a5..000000000 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.md +++ /dev/null @@ -1,203 +0,0 @@ -# PR2 — JAG 合宿系と JAG 模擬地区を問題記号にする(66 コンテスト / 625 問) - -AOJ の JAG 合宿系(夏 / 冬 / 春)と JAG 模擬地区の `task_table_index` を、AOJ の問題 ID から問題記号(A, B, C…)に変える。PR1 で ICPC / JAG 模擬国内に行ったことを、残りの JAG に広げる。 - -全体の目的・共通の決定(M 番号)・データ移行の共通手順は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 - -**状態**: 実装済み(ブランチ `#4030`、未マージ)。[実施記録](#実施記録2026-10-25) - -## 範囲 - -| 範囲 | コンテスト / 問題 | ある場所 | 今の `task_table_index` | 今の `title` | -| -------------------- | ----------------- | ---------------------------------- | ----------------------- | --------------------------------- | -| JAG 夏 / 冬 / 春合宿 | 50 / 467 | seed・staging・本番 | 問題 ID | `'2389. …'`(問題 ID が前に付く) | -| JAG 模擬地区 | 16 / 158 | staging・本番(**seed には無い**) | 問題 ID | 素 | - -PR2 の後は、625 問すべてで `task_table_index` が問題記号になり、合宿系の title は素になる。管理画面の取り込みでも、JAG は全 round で記号を付けるようになる。 - -範囲外: - -- コンテスト別のテーブルの作成(PR2.5) -- DB に無いコンテストの追加(PR2.1) -- AOJ のコンテスト一覧に無い問題の補完(PR3) -- 前後に空白のある title(本番 46 件。3355 の末尾のタブを含む。[plan.md](./plan.md#スコープ外)) - -## 知っておくこと - -- **既存の行は自動では変わらない**。seed も取り込みも「既存なら skip」なので、移行 SQL で直す(M2) -- **記号は基本ルール(コンテスト内で問題 ID の昇順に A, B, C…)で付く。ただし 8 コンテストは例外**。先頭や途中の問題が AOJ に無く、そのまま振ると記号がずれる。正しい記号はメンテナのスプレッドシートの位置で決める(M13。一覧は [investigation.md](./investigation.md#記号の補正が要るコンテスト)。8 コンテスト / 45 行) -- **合宿系の title は直す**。index だけ記号にすると、表示側の `removeTaskIndexFromTitle`(先頭の `${index}. ` を外す)が空振りし、`'2389. …'` が出典欄やセルに出る。467 件すべてで、前に付いた番号は自分の `task_id` と一致する(M9) -- **一意性は壊れない**。`id` / `task_id` は `task_table_index` を使っておらず、FK もすべて `task_id` を参照する([根拠](./investigation.md#一意性は壊れない)) -- **データの状態**: 本番と staging は同じで(2026-10-24 に集計。SQL は[確かめ方](#確かめ方))、範囲の全行で index が問題 ID のまま。title に問題 ID が付いているのは合宿系だけ - -| kind | 本番・staging(コンテスト / 問題) | index が問題 ID のまま | title が問題 ID 付き | -| -------- | ---------------------------------- | ---------------------- | -------------------- | -| Regional | 16 / 158 | 全行 | 0 行 | -| Spring | 4 / 42 | 全行 | 全行 | -| Summer | 36 / 338 | 全行 | 全行 | -| Winter | 10 / 87 | 全行 | 全行 | - -## タスク - -| # | 内容 | 補足 | -| --- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -| 2-1 | seed に模擬地区 158 問を追加する(M12) | [下記](#2-1-seed-に模擬地区を足す) | -| 2-2 | 対応表を作り、スプレッドシートと照合する(M6) | [下記](#2-2-照合) | -| 2-3 | 取り込みで、JAG は全 round で記号を付ける(M14) | `AojChallengesApiClient` の `usesProblemLetters` を `contestType === 'ICPC' \|\| contestType === 'JAG'` にする | -| 2-4 | `prisma/tasks.ts` の 625 件の `problem_index` と、合宿系 467 件の `title` を書き換える | 2-2 の出力で置き換える | -| 2-5 | 移行 SQL | [下記](#2-5-移行-sql) | -| 2-6 | テスト | [下記](#2-6-テスト) | -| 2-7 | 手順書 | `add-tasks` の「ICPC / JAG 模擬国内の `problem_index` は問題記号にする」を JAG 全体に広げる | -| 2-8 | 照合スクリプトを削除する | 追加とは別のコミットにし、git の履歴から辿れるようにする | - -**クロスレビューが必須**(データ移行)。 - -### 2-1 seed に模擬地区を足す - -本番から次の SQL で取り出した値を、まず本番と同じ形(index = 問題 ID)で `prisma/tasks.ts` に足す。2-2 の照合はこれを読む。記号への書き換えは 2-4 で合宿系と一緒に行う。 - -```sql --- JAG Regional tasks for the seed (run on production). -SELECT task_id, contest_id, task_table_index, title, grade -FROM task -WHERE contest_type = 'AOJ_JAG' - AND contest_id LIKE 'JAGRegional%' -ORDER BY contest_id, task_id::int; -``` - -- grade は本番の値のまま入れる(PENDING でないのは 6 問: 2251 D3、2594 D3、2736 D5、2858 D1、2863 D2、2865 D4) -- title は取り込みと同じく trim する(3355 の末尾のタブを落とす) - -### 2-2 照合 - -PR1 のスクリプト(`git show 171bf3c9:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_problem_letters.ts`。仕組みは [done.md](./done.md#検証で分かったこと))を次のように直して使う。 - -- 対象は `^JAG(Summer|Winter|Spring|Regional)` の 66 コンテスト / 625 問。件数が合わなければ止める -- スプレッドシートは 2026-10-24 のスクリーンショット(16 枚、行 2〜1025)から書き写す。PR2.1 の 10 コンテストは含めない -- 記号はスプレッドシートの位置(欠けている問題を飛ばした位置)から決める。PR1 は基本ルールで記号を付けてから照合したが、PR2 には先頭・途中の欠落があるのでそうはいかない -- 基本ルールと違うコンテストを別に出力し、2-5 の補正の `VALUES` に使う。期待する結果は 8 コンテスト / 45 行(目視の暫定値。合わなければ、どちらが正しいかを個別に確かめる) -- 夏合宿 2010 Day2 の J(`Person responsible for problem description don't w`)は、AOJ の title 自体も同じ所で切れている。前方一致の扱いは要らない - -### 2-5 移行 SQL - -骨組みのみ。補正の `VALUES`(45 行)は 2-2 の出力で埋める。3 つの UPDATE を 1 つの DO ブロックに入れ、それぞれの更新件数を検査する。想定外の件数なら RAISE して、マイグレーション全体を戻す(M13。UPDATE は対象が 0 件でもエラーにならないため)。 - -```sql -DO $$ -DECLARE - updated_count integer; -BEGIN - -- (1) Basic rule, as in PR1, for rows that still hold the AOJ problem id. - WITH letters AS ( - SELECT task_id, - chr(64 + row_number() OVER ( - PARTITION BY contest_id - ORDER BY task_id::int - )::int) AS letter - FROM "task" - WHERE contest_type = 'AOJ_JAG' - AND contest_id ~ '^JAG(Summer|Winter|Spring|Regional)' - ) - UPDATE "task" AS t - SET "task_table_index" = letters.letter - FROM letters - WHERE t."task_id" = letters.task_id - AND t."contest_type" = 'AOJ_JAG' - AND t."contest_id" ~ '^JAG(Summer|Winter|Spring|Regional)' - -- Rank every row above, but leave rows that already hold a letter (added with letters) as they are. - AND t."task_table_index" = t."task_id"; - - GET DIAGNOSTICS updated_count = ROW_COUNT; - -- 0: empty DB (migrate reset), 467: seed without JAG Regional, 625: staging / production. - IF updated_count NOT IN (0, 467, 625) THEN - RAISE EXCEPTION 'basic rule: unexpected row count %', updated_count; - END IF; - - -- (2) Overwrite every row of the contests that have problems missing from AOJ. - UPDATE "task" AS t - SET "task_table_index" = fixes.letter - FROM (VALUES - ('JAGSummer2006-day1', '2057', 'B') - -- ... 45 rows in total - ) AS fixes (contest_id, task_id, letter) - WHERE t."task_id" = fixes.task_id - AND t."contest_id" = fixes.contest_id - AND t."contest_type" = 'AOJ_JAG'; - - GET DIAGNOSTICS updated_count = ROW_COUNT; - -- 32: training camps only, 45: training camps and JAG Regional. - IF updated_count NOT IN (0, 32, 45) THEN - RAISE EXCEPTION 'overrides: unexpected row count %', updated_count; - END IF; - - -- (3) Strip the legacy ". " prefix from titles (M9). - UPDATE "task" - SET "title" = substr("title", length("task_id") + 3) - WHERE "contest_type" = 'AOJ_JAG' - AND "contest_id" ~ '^JAG(Summer|Winter|Spring|Regional)' - AND starts_with("title", "task_id" || '. '); - - GET DIAGNOSTICS updated_count = ROW_COUNT; - IF updated_count NOT IN (0, 467) THEN - RAISE EXCEPTION 'titles: unexpected row count %', updated_count; - END IF; -END $$; -``` - -- **範囲は `contest_id` の接頭辞で絞る**。`AOJ_JAG` には、PR1 で記号にした模擬国内も含まれる -- **件数の想定値**: staging / 本番は 625 / 45 / 467(上の表から)。0 を含めるのは、`prisma migrate reset`(ローカル)が空の DB にマイグレーションを流してから seed を入れるため。開発者の既存の DB は合宿系だけを持つので 467 / 32 / 467 になる -- **title の条件**: 「自分の `task_id` + `. `」で始まるものだけを対象にする。`~ '^[0-9]+\. '` だと、番号で始まる本来の title まで削りうる -- **`task_table_index = task_id` の条件**: PR2.1 の行(記号付き・素の title で入る)が先にあっても、(1)(3) の対象にならない。PR2.1 と順序を問わないための保険。順位はコンテストの全行で付け、更新だけをこの条件で絞る。順位を付ける側でも絞ると、記号の行と問題 ID の行が混ざったコンテストで A から振り直してしまう(Codex のレビューで指摘) -- `task_id::int` は安全(範囲の問題 ID はすべて数値)。1 コンテスト最大 14 問なので、`chr(64 + n)` は A〜Z に収まる。SQL の読み方は [investigation.md](./investigation.md#sql-の読み方) - -#### 確かめ方 - -[共通の手順](./plan.md#手順)(ローカルで `UPDATE` を `SELECT` に置き換えて「現在値 → 変更後」を見る、など)に加えて: - -- PR2 の seed の模擬地区は記号で入っている(2-1 で足し、2-4 で記号にする)ので、seed から作った DB では移行 SQL の模擬地区の分を確かめられない。PR2 のコードに切り替える前の DB に、管理画面から JAG REGIONAL を取り込んでおく(問題 ID のまま入る)。そうすればローカルでも 625 / 45 / 467 になる -- 補正する 8 コンテストは、「現在値 → 変更後」を全行見る -- 問題一覧の出典欄で、合宿系・模擬地区が `JAG 夏合宿 2024 Day3 - H(AOJ)` のように記号になり、title に問題 ID が出ていないこと(この範囲のコンテストテーブルは PR2.5 まで無い) -- **PR を開く前に**、staging で次の SQL を流し、上の表と同じ件数であることを確かめる。PR の CI は、staging へのマージ前に staging の DB へマイグレーションを流す([CI での適用](./plan.md#ci-での適用))。件数が違うと RAISE でデプロイが止まる。**済(2026-10-24)**: 上の表と同じ件数だった(Regional 16 / 158、Spring 4 / 42、Summer 36 / 338、Winter 10 / 87。index は全行が問題 ID のまま、title の問題 ID は合宿系の全行だけ)。PR を開くまでに staging の AOJ の問題を足したり消したりした場合は、もう一度流す - -```sql --- State of the PR2 scope (run on staging before opening the PR). -SELECT substring(contest_id FROM '^JAG(Summer|Winter|Spring|Regional)') AS kind, - count(DISTINCT contest_id) AS contests, - count(*) AS tasks, - count(*) FILTER (WHERE task_table_index = task_id) AS index_is_problem_id, - count(*) FILTER (WHERE starts_with(title, task_id || '. ')) AS title_has_id_prefix -FROM task -WHERE contest_type = 'AOJ_JAG' - AND contest_id ~ '^JAG(Summer|Winter|Spring|Regional)' -GROUP BY kind -ORDER BY kind; -``` - -### 2-6 テスト - -- `src/lib/clients/aizu_online_judge/seed_problem_letters.test.ts` の範囲を 145 コンテスト / 1,257 問に広げる。補正していないコンテストは `assignProblemLetters` と一致すること、補正した 8 コンテストは literal の期待値と一致することを見る(M18)。title が問題名(`name`)と同じであることも見る(`${problem id}. ` の付け忘れ・除き過ぎの両方を検出する) -- `clients.test.ts`: JAG REGIONAL の取り込みで記号が付くこと。`usesProblemLetters` の JAG の条件を PRELIM だけに戻す変異を検出できること -- 書き換えたテストと足したテストに `verify-test-strength` を当てる。期待値を新しい形へ一括置換すると、テストが仕様を検証しなくなるので、`testing.md` の Test integrity に従う - -## 実施記録(2026-10-25) - -コミットは `c605d3bd`(照合スクリプト)→ `4c7b9763`(実装)→ `c835754b`(レビュー対応)→ `597104fd`(スクリプトの削除)。照合スクリプトは `git show c605d3bd:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_jag_problem_letters.ts` で参照できる。 - -- **seed の模擬地区**: 本番から取り出した 158 問は、AOJ の fixture(2026-05 記録)と問題 ID・コンテスト・名前がすべて一致した -- **照合**: スプレッドシートの書き写し(66 コンテスト)と 625 問の title が、記号の位置ですべて一致した。補正は 8 コンテスト / 45 行で、目視の暫定値どおり。わざと誤りを入れたコピー 5 種類(欠落の記号を外す、順番の入れ替え、途中の title の誤り、欠落の数の不足、切れた title の前半の誤り)は、すべて検出された -- **ローカルでの移行**: 管理画面で模擬地区を取り込み、本番と同じ状態(16 / 158、index は問題 ID のまま)にしてから確かめた。`SELECT` のプレビューは 625 行すべてで照合の結果と一致し、適用後もプレビューと 625 / 625 で一致した。範囲外の AOJ(PCK、大学系、PR1 の範囲)は変わらない。`EXPLAIN` は CTE 側の Seq Scan 1 回と、`task_task_id_key` の Index Scan を使う Nested Loop -- **テストの検出力**: 6 つの変異をすべて検出した(JAG の記号を PRELIM だけ / ICPC だけにする、補正した記号を基本ルールに戻す、基本ルールのコンテストで A・B を入れ替える、title に問題 ID を付け直す、模擬地区の記号を問題 ID に戻す)。title のテストは、空の title・別の問題の title・問題 ID の付け直しも検出する -- **title の空白**: ローカルの 3355 にも末尾のタブがあった(取り込みが trim する前に入った行)。本件では直さない([plan.md](./plan.md#スコープ外)) - -Codex のクロスレビュー(2026-10-25): - -| 指摘 | 扱い | -| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Moderate: 取り込みは記号がずれる 8 コンテストでも基本ルールで振る | **対応しない**。M14 の決定どおり。8 コンテストは本番・staging・seed のすべてにあり、取り込みは既存の問題を skip する | -| Minor: title のテストが「問題 ID が前に付いていない」しか見ない | **対応**。「title が name と同じ」を見るテストに変えた(seed の 1,257 問すべてで成り立つ) | -| Moderate(条件付き): (1) が問題 ID の行だけで順位を付けるので、記号の行と混ざったコンテストで A から振り直す | **対応**。順位は全行で付け、更新だけ問題 ID の行に絞った。混ざったコンテストを読み取り専用で再現し、直した SQL が正しく振ることを確かめた。ローカルは移行前の状態に戻してから、プレビューと適用をやり直した(625 / 625 で一致) | -| Moderate(条件付き): `task_id` だけで結合している | **対応しない**。`task_id` は schema で `@unique` | - -残っている確認: staging(PR の CI)と本番で、移行が想定の件数(625 / 45 / 467)で通ること。問題一覧の出典欄の表示。 - -本番に入ったら、このファイルを [done.md](./done.md) にまとめる(PR1.5 と同じく、決定とコードから読めない事実だけを残し、このファイルは消してリンクを張り替える)。 diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md index c6b1d10ee..dcf0899bd 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md @@ -1,17 +1,16 @@ # AOJ ICPC / JAG の `problem_index` を A/B/C 形式へ変更する -**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2 は実装済み(ブランチ `#4030`、未マージ) +**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)は staging に反映、本番は未反映 このファイルは全体像と、PR をまたぐ決定・手順だけを書く。各 PR の作業はそれぞれのファイルにある。 -| ファイル | 内容 | -| -------------------------------------- | ---------------------------------------------------- | -| [plan-pr2.md](./plan-pr2.md) | PR2: 合宿系と JAG 模擬地区を記号にする(データ移行) | -| [plan-pr2-1.md](./plan-pr2-1.md) | PR2.1: DB に無い 10 コンテストを入れる | -| [plan-pr2-5.md](./plan-pr2-5.md) | PR2.5: コンテスト別のテーブルの作成(Provider) | -| [plan-pr3.md](./plan-pr3.md) | PR3: 欠落問題の補完 | -| [done.md](./done.md) | PR0 / PR1 / PR1.5 の決定の要約と残タスク | -| [investigation.md](./investigation.md) | 根拠データ。PR2 の照合結果、表示順、欠落の一覧など | +| ファイル | 内容 | +| -------------------------------------- | -------------------------------------------------- | +| [plan-pr2-1.md](./plan-pr2-1.md) | PR2.1: DB に無い 10 コンテストを入れる | +| [plan-pr2-5.md](./plan-pr2-5.md) | PR2.5: コンテスト別のテーブルの作成(Provider) | +| [plan-pr3.md](./plan-pr3.md) | PR3: 欠落問題の補完 | +| [done.md](./done.md) | PR0 / PR1 / PR1.5 / PR2 の決定の要約と残タスク | +| [investigation.md](./investigation.md) | 根拠データ。PR2 の照合結果、表示順、欠落の一覧など | ## ゴール @@ -36,19 +35,21 @@ AOJ の API は 2026-09-07 に別のホストへ移り、旧ホストは 410 を返す。PR1.5(#4219)で追従し、staging では取り込めることを確かめた。本番は `main` への昇格まで取り込めない。 +PR2(#4220)で、合宿系と模擬地区も記号になり、合宿系の title は素になった。管理画面の取り込みも、JAG は全 round で記号を付ける(staging に反映。本番は未反映)。 + ## PR の分割と順序 -| | 範囲 | コンテスト | 問題数 | 内容 | -| ----------------------------------------------------- | ------------------------------------------------------- | ---------- | ------------------------- | --------------------------------------------------------- | -| [PR1.5](./done.md#pr15--aoj-api-の移転に追従する4028) | AOJ の API クライアント | — | — | ベース URL を新しいホストへ変える(#4028)。完了(#4219) | -| [PR2](./plan-pr2.md) | JAG 夏合宿 / 冬合宿 / 春合宿 + JAG 模擬地区 | 66 | 625 | 既存データを記号にする(seed・移行 SQL・title・取り込み) | -| [PR2.1](./plan-pr2-1.md) | 2025 / 2026 の夏合宿(AOJ)+ in AtCoder の 4 コンテスト | 10 | 114(スプレッドシート上) | 新しいデータを入れる(`ATCODER_JAG` の新設を含む) | -| [PR2.5](./plan-pr2-5.md) | PR2 / PR2.1 の 76 コンテスト | 76 | — | グループ「JAG 模擬地区」に、コンテスト別のテーブルを作る | -| [PR3](./plan-pr3.md) | 欠落問題の補完 | — | — | AOJ のコンテスト一覧に無い問題をどう埋めるか | +| | 範囲 | コンテスト | 問題数 | 内容 | +| ------------------------------------------------------------------- | ------------------------------------------------------- | ---------- | ------------------------- | --------------------------------------------------------- | +| [PR1.5](./done.md#pr15--aoj-api-の移転に追従する4028) | AOJ の API クライアント | — | — | ベース URL を新しいホストへ変える(#4028)。完了(#4219) | +| [PR2](./done.md#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030) | JAG 夏合宿 / 冬合宿 / 春合宿 + JAG 模擬地区 | 66 | 625 | 既存データを記号にする。staging に反映(#4220) | +| [PR2.1](./plan-pr2-1.md) | 2025 / 2026 の夏合宿(AOJ)+ in AtCoder の 4 コンテスト | 10 | 114(スプレッドシート上) | 新しいデータを入れる(`ATCODER_JAG` の新設を含む) | +| [PR2.5](./plan-pr2-5.md) | PR2 / PR2.1 の 76 コンテスト | 76 | — | グループ「JAG 模擬地区」に、コンテスト別のテーブルを作る | +| [PR3](./plan-pr3.md) | 欠落問題の補完 | — | — | AOJ のコンテスト一覧に無い問題をどう埋めるか | PR1 と PR2 で、記号にするのは 1,257 問 / 145 コンテストになる。PR2 で完結し、PR3 に進まなくても機能として成立する。 -- **PR1.5 → staging で JAG REGIONAL を取り込む → PR2 の順にする**。PR2 の件数チェックを staging で通すには、staging の模擬地区を本番と同じ 16 / 158 にそろえる必要があり、それには AOJ の取り込みが動いている必要がある。取り込みは PR2 のコードが入る前に行う(PR2 のコードで取り込むと基本ルールの記号が入り、2005 / 2013 がずれる) +- **PR1.5 → staging で JAG REGIONAL を取り込む → PR2** の順で進めた(済)。PR2 の件数チェックを staging で通すには、staging の模擬地区を本番と同じ 16 / 158 にそろえる必要があったため - **PR2 → PR2.5 は守る**。Provider を先に出すと、合宿系の数値の index と `'2389. …'` の title がそのまま表に出る - **PR2.1 は PR2 / PR2.5 と前後してよい**。PR2 の移行 SQL は index が問題 ID のままの行しか書き換えず、Provider はデータの無いコンテストを表示しない(`TaskTable.svelte` の `prepareContestTable` が `null` を返す) - **PR3 は独立**。PR2 で記号を補正するので、欠落を後から足しても既存の記号はずれない From 524b800a3cf5382843579ddc0f2a338cb3da355a Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:11:00 +0000 Subject: [PATCH 2/7] feat(contests): add ATCODER_JAG contest type for JAG contests held on AtCoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classify four JAG summer camp days held on AtCoder (jag2018summer-day2, jag2017summer-day1, jag2015summer-day3, jag2013summer-warmingup) by exact contest_id, so the AtCoder import stops skipping them. AtCoder's JAG2013Spring / JAG2014Spring stay unclassified because AOJ already covers them as JAGSpring2013 / 2014. ATCODER_JAG sits right before UNIVERSITY in the enum, the priorities (18, shifting UNIVERSITY and later by one) and the classification rules. Labels come from the contest_id map, e.g. 'JAG 夏合宿 2013 Day2 WarmUp'. Task URLs stay on AtCoder, including task ids that start with "icpc". Refs #4221 Co-Authored-By: Claude Opus 5.5 --- .../migration.sql | 2 ++ prisma/schema.prisma | 1 + .../contests/fixtures/contest_name_labels.ts | 19 ++++++++++++ src/lib/contests/fixtures/contest_type.ts | 19 ++++++++++++ src/lib/contests/types/contest.ts | 1 + src/lib/contests/utils/classification.test.ts | 15 ++++++++++ src/lib/contests/utils/classification.ts | 2 ++ src/lib/contests/utils/labels/index.test.ts | 8 +++++ src/lib/contests/utils/labels/index.ts | 3 +- src/lib/contests/utils/prefixes.ts | 9 ++++++ src/lib/contests/utils/priority.test.ts | 5 ++++ src/lib/contests/utils/priority.ts | 29 ++++++++++--------- src/test/lib/utils/task.test.ts | 8 +++++ src/test/lib/utils/test_cases/task_url.ts | 14 +++++++++ 14 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/migration.sql diff --git a/prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/migration.sql b/prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/migration.sql new file mode 100644 index 000000000..be5a794dc --- /dev/null +++ b/prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ContestType" ADD VALUE 'ATCODER_JAG'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ce6531e01..4ab0af07a 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -312,6 +312,7 @@ enum ContestType { ARC_LIKE // AtCoder Regular Contest (ARC) 相当のコンテスト AGC_LIKE // AtCoder Grand Contest (AGC) 相当のコンテスト AWC // AtCoder Weekday Contest + ATCODER_JAG // JAG (ICPC Japan Alumni Group) contests held on AtCoder (e.g., summer camp days) UNIVERSITY // University Programming Contests in AtCoder (e.g., UTPC) FPS_24 // 24 Problems on Formal Power Series GAME_24 // 24 Problems on Combinatorial Games diff --git a/src/lib/contests/fixtures/contest_name_labels.ts b/src/lib/contests/fixtures/contest_name_labels.ts index 82be1f032..d7457f50f 100644 --- a/src/lib/contests/fixtures/contest_name_labels.ts +++ b/src/lib/contests/fixtures/contest_name_labels.ts @@ -35,6 +35,25 @@ export const aclPractice = [ }), ]; +export const atCoderJag = [ + createTestCaseForContestNameLabel('JAG Summer Camp 2018 Day2')({ + contestId: 'jag2018summer-day2', + expected: 'JAG 夏合宿 2018 Day2', + }), + createTestCaseForContestNameLabel('JAG Summer Camp 2017 Day1')({ + contestId: 'jag2017summer-day1', + expected: 'JAG 夏合宿 2017 Day1', + }), + createTestCaseForContestNameLabel('JAG Summer Camp 2015 Day3')({ + contestId: 'jag2015summer-day3', + expected: 'JAG 夏合宿 2015 Day3', + }), + createTestCaseForContestNameLabel('JAG Summer Camp 2013 Day2 WarmUp')({ + contestId: 'jag2013summer-warmingup', + expected: 'JAG 夏合宿 2013 Day2 WarmUp', + }), +]; + export const atCoderOthers = [ createTestCaseForContestNameLabel('Chokudai SpeedRun 001')({ contestId: 'chokudai_S001', diff --git a/src/lib/contests/fixtures/contest_type.ts b/src/lib/contests/fixtures/contest_type.ts index cd5131ca9..50f339cfb 100644 --- a/src/lib/contests/fixtures/contest_type.ts +++ b/src/lib/contests/fixtures/contest_type.ts @@ -399,6 +399,25 @@ export const awc = awcContestIds.map((contestId) => // // See: // https://kenkoooo.com/atcoder/resources/contests.json +export const atCoderJag = [ + createTestCaseForContestType('JAG Summer Camp 2018 Day2')({ + contestId: 'jag2018summer-day2', + expected: ContestType.ATCODER_JAG, + }), + createTestCaseForContestType('JAG Summer Camp 2017 Day1')({ + contestId: 'jag2017summer-day1', + expected: ContestType.ATCODER_JAG, + }), + createTestCaseForContestType('JAG Summer Camp 2015 Day3')({ + contestId: 'jag2015summer-day3', + expected: ContestType.ATCODER_JAG, + }), + createTestCaseForContestType('JAG Summer Camp 2013 Day2 WarmUp')({ + contestId: 'jag2013summer-warmingup', + expected: ContestType.ATCODER_JAG, + }), +]; + const universityContestIds = [ 'fuka5', 'kupc2012', diff --git a/src/lib/contests/types/contest.ts b/src/lib/contests/types/contest.ts index affde19ee..dc9abc49f 100644 --- a/src/lib/contests/types/contest.ts +++ b/src/lib/contests/types/contest.ts @@ -46,6 +46,7 @@ export const ContestType: { [key in ContestTypeOrigin]: key } = { ARC_LIKE: 'ARC_LIKE', // AtCoder Regular Contest (ARC) 相当のコンテスト AGC_LIKE: 'AGC_LIKE', // AtCoder Grand Contest (AGC) 相当のコンテスト AWC: 'AWC', // AtCoder Weekday Contest + ATCODER_JAG: 'ATCODER_JAG', // JAG (ICPC Japan Alumni Group) contests held on AtCoder (e.g., summer camp days) UNIVERSITY: 'UNIVERSITY', // University Programming Contests in AtCoder (e.g., UTPC) FPS_24: 'FPS_24', // 24 Problems on Formal Power Series GAME_24: 'GAME_24', // 24 Problems on Combinatorial Games diff --git a/src/lib/contests/utils/classification.test.ts b/src/lib/contests/utils/classification.test.ts index 9901b35fb..7f45b35f4 100644 --- a/src/lib/contests/utils/classification.test.ts +++ b/src/lib/contests/utils/classification.test.ts @@ -176,6 +176,21 @@ describe('classify contest', () => { }); }); + describe('when contest_id means a JAG contest held on AtCoder', () => { + TestCasesForContestType.atCoderJag.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(classifyContest(contestId)).toEqual(expected); + }); + }); + }); + + // The same contests as AOJ JAGSpring2013 / 2014; importing them from AtCoder would duplicate the problems. + describe('when contest_id is a JAG contest on AtCoder that AOJ already covers', () => { + test.each(['JAG2013Spring', 'JAG2014Spring'])('returns null for %s', (contestId) => { + expect(classifyContest(contestId)).toBeNull(); + }); + }); + describe('when contest_id matches contests held by university students', () => { TestCasesForContestType.universities.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { diff --git a/src/lib/contests/utils/classification.ts b/src/lib/contests/utils/classification.ts index 7fa0786fe..91d43b0ce 100644 --- a/src/lib/contests/utils/classification.ts +++ b/src/lib/contests/utils/classification.ts @@ -5,6 +5,7 @@ import { abcLikePrefixes, arcLikePrefixes, agcLikePrefixes, + atCoderJagPrefixes, atCoderUniversityPrefixes, atCoderOthersPrefixes, atCoderMainOfficialOnsitePrefixes, @@ -61,6 +62,7 @@ const CLASSIFICATION_RULES: readonly ClassificationRule[] = [ { matches: (id) => arcLikePrefixes.has(id), type: ContestType.ARC_LIKE }, // Prefix-based matches { matches: (id) => agcLikePrefixes.some((p) => id.startsWith(p)), type: ContestType.AGC_LIKE }, + { matches: (id) => atCoderJagPrefixes.has(id), type: ContestType.ATCODER_JAG }, { matches: (id) => atCoderUniversityPrefixes.some((p) => id.startsWith(p)), type: ContestType.UNIVERSITY, diff --git a/src/lib/contests/utils/labels/index.test.ts b/src/lib/contests/utils/labels/index.test.ts index 5366b0dc0..8bf9925a6 100644 --- a/src/lib/contests/utils/labels/index.test.ts +++ b/src/lib/contests/utils/labels/index.test.ts @@ -122,6 +122,14 @@ describe('get contest name label', () => { }); }); + describe('when contest_id means a JAG contest held on AtCoder', () => { + TestCasesForContestNameLabel.atCoderJag.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { + expect(getContestNameLabel(contestId)).toEqual(expected); + }); + }); + }); + describe('when contest_id means AtCoder official onsite finals (e.g., World Tour Finals, CodeQUEEN)', () => { TestCasesForContestNameLabel.atCoderMainOfficialOnsite.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { diff --git a/src/lib/contests/utils/labels/index.ts b/src/lib/contests/utils/labels/index.ts index 8de8f0554..5c6a3f584 100644 --- a/src/lib/contests/utils/labels/index.ts +++ b/src/lib/contests/utils/labels/index.ts @@ -1,7 +1,7 @@ import { ContestType } from '#lib/contests/types/contest.js'; import { classifyContest } from '../classification'; -import { AOJ_COURSES, ATCODER_MAIN_OFFICIAL_ONSITE } from '../prefixes'; +import { AOJ_COURSES, ATCODER_JAG, ATCODER_MAIN_OFFICIAL_ONSITE } from '../prefixes'; import { generateAxcLabel, generateAwcLabel } from './axc'; import { getAtCoderUniversityContestLabel } from './universities'; import { getWorldTourFinalsLabel, isWorldTourFinals } from './world_tour_finals'; @@ -41,6 +41,7 @@ export const LABEL_GENERATORS: ReadonlyMap = new Ma (id) => isWorldTourFinals(id) ? getWorldTourFinalsLabel(id) : ATCODER_MAIN_OFFICIAL_ONSITE[id], ], + [ContestType.ATCODER_JAG, (id) => ATCODER_JAG[id]], [ContestType.UNIVERSITY, (id) => getAtCoderUniversityContestLabel(id)], [ContestType.OTHERS, generateOthersLabel], [ContestType.AOJ_COURSES, (id) => getAojContestLabel(AOJ_COURSES, id)], diff --git a/src/lib/contests/utils/prefixes.ts b/src/lib/contests/utils/prefixes.ts index 0c476ee00..3a3590dca 100644 --- a/src/lib/contests/utils/prefixes.ts +++ b/src/lib/contests/utils/prefixes.ts @@ -51,6 +51,14 @@ export const AGC_LIKE: ContestPrefix = { 'cf17-final': 'CODE FESTIVAL 2017 final', } as const; +// JAG contests held on AtCoder, matched exactly: AtCoder's JAG2013Spring / JAG2014Spring duplicate AOJ JAGSpring2013 / 2014. +export const ATCODER_JAG: ContestPrefix = { + 'jag2018summer-day2': 'JAG 夏合宿 2018 Day2', + 'jag2017summer-day1': 'JAG 夏合宿 2017 Day1', + 'jag2015summer-day3': 'JAG 夏合宿 2015 Day3', + 'jag2013summer-warmingup': 'JAG 夏合宿 2013 Day2 WarmUp', +} as const; + // HACK: As of October 2026, KUPC, QUPC, UTPC, TTPC, TUPC, WUPC, fuka and bogosort contests are included. // More university contests may be added in the future. // Values serve as labels for contest IDs that don't match the `xxpc{year}` label regex. @@ -143,6 +151,7 @@ export function getContestPrefixes(contestPrefixes: Record) { export const abcLikePrefixes = new Set(getContestPrefixes(ABC_LIKE)); export const arcLikePrefixes = new Set(getContestPrefixes(ARC_LIKE)); export const agcLikePrefixes = getContestPrefixes(AGC_LIKE); +export const atCoderJagPrefixes = new Set(getContestPrefixes(ATCODER_JAG)); export const atCoderUniversityPrefixes = getContestPrefixes(ATCODER_UNIVERSITIES); export const atCoderOthersPrefixes = getContestPrefixes(ATCODER_OTHERS); export const atCoderMainOfficialOnsitePrefixes = new Set( diff --git a/src/lib/contests/utils/priority.test.ts b/src/lib/contests/utils/priority.test.ts index 4db62d293..1c8b7a048 100644 --- a/src/lib/contests/utils/priority.test.ts +++ b/src/lib/contests/utils/priority.test.ts @@ -17,6 +17,7 @@ const contestIdsInDisplayOrder = [ 'acl1', 'cf16-final', 'awc0001', + 'jag2018summer-day2', 'utpc2023', 'atc001', 'ITP1', @@ -132,5 +133,9 @@ describe('contest type priorities', () => { test('ranks AALC directly below GAME_24', () => { expect(priorityOf(ContestType.AALC)).toBe(priorityOf(ContestType.GAME_24) + 1); }); + + test('ranks ATCODER_JAG directly above UNIVERSITY', () => { + expect(priorityOf(ContestType.ATCODER_JAG)).toBe(priorityOf(ContestType.UNIVERSITY) - 1); + }); }); }); diff --git a/src/lib/contests/utils/priority.ts b/src/lib/contests/utils/priority.ts index 1d8883d72..4498fdd8f 100644 --- a/src/lib/contests/utils/priority.ts +++ b/src/lib/contests/utils/priority.ts @@ -2,13 +2,13 @@ import { ContestType } from '#lib/contests/types/contest.js'; import { classifyContest } from './classification'; /** - * Contest type priorities (0 = Highest, 28 = Lowest) + * Contest type priorities (0 = Highest, 29 = Lowest) * * Priority assignment rationale: * - Educational contests (0-11, 17): ABS, ABC, APG4B and AWC etc. * - Contests for genius (12-16): ARC, AGC, and their variants - * - Special contests (18-23): UNIVERSITY, FPS_24, GAME_24, AALC, ATCODER_MAIN_OFFICIAL_ONSITE, OTHERS - * - External platforms (24-28): AOJ_COURSES, AOJ_PCK, AOJ_ICPC, AOJ_JAG, AOJ_UNIVERSITY + * - Special contests (18-24): ATCODER_JAG, UNIVERSITY, FPS_24, GAME_24, AALC, ATCODER_MAIN_OFFICIAL_ONSITE, OTHERS + * - External platforms (25-29): AOJ_COURSES, AOJ_PCK, AOJ_ICPC, AOJ_JAG, AOJ_UNIVERSITY * * @remarks * HACK: The priorities for ARC, AGC, UNIVERSITY, AOJ_COURSES, and AOJ_PCK are temporary @@ -36,17 +36,18 @@ export const contestTypePriorities: Map = new Map([ [ContestType.ARC_LIKE, 15], [ContestType.AGC_LIKE, 16], [ContestType.AWC, 17], - [ContestType.UNIVERSITY, 18], - [ContestType.FPS_24, 19], - [ContestType.GAME_24, 20], - [ContestType.AALC, 21], - [ContestType.ATCODER_MAIN_OFFICIAL_ONSITE, 22], - [ContestType.OTHERS, 23], // AtCoder (その他) - [ContestType.AOJ_COURSES, 24], - [ContestType.AOJ_PCK, 25], - [ContestType.AOJ_ICPC, 26], - [ContestType.AOJ_JAG, 27], - [ContestType.AOJ_UNIVERSITY, 28], + [ContestType.ATCODER_JAG, 18], + [ContestType.UNIVERSITY, 19], + [ContestType.FPS_24, 20], + [ContestType.GAME_24, 21], + [ContestType.AALC, 22], + [ContestType.ATCODER_MAIN_OFFICIAL_ONSITE, 23], + [ContestType.OTHERS, 24], // AtCoder (その他) + [ContestType.AOJ_COURSES, 25], + [ContestType.AOJ_PCK, 26], + [ContestType.AOJ_ICPC, 27], + [ContestType.AOJ_JAG, 28], + [ContestType.AOJ_UNIVERSITY, 29], ]); /** Priority given to a contest_id that no classification rule matches, so it sorts last. */ diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index acde52276..b872b3d7d 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -76,6 +76,14 @@ describe('Task', () => { }); }); + describe('when contest ids and task ids for JAG contests held on AtCoder are given', () => { + TestCasesForTaskUrl.atCoderJagTasks.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, taskId, expected }: TestCaseForTaskUrl) => { + expect(getTaskUrl(contestId, taskId)).toBe(expected); + }); + }); + }); + describe('when contest ids and task ids for AOJ courses are given', () => { TestCasesForTaskUrl.aojCourses.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, taskId, expected }: TestCaseForTaskUrl) => { diff --git a/src/test/lib/utils/test_cases/task_url.ts b/src/test/lib/utils/test_cases/task_url.ts index 4a7bf2a83..f6d227792 100644 --- a/src/test/lib/utils/test_cases/task_url.ts +++ b/src/test/lib/utils/test_cases/task_url.ts @@ -83,6 +83,20 @@ const educationalTasks = [ export const atCoderTasks = [...abc365Tasks, ...beginnersTasks, ...educationalTasks]; +// JAG contests held on AtCoder; some task ids start with "icpc" but still belong to AtCoder. +export const atCoderJagTasks = [ + createTestCaseForTaskUrl('JAG Summer Camp 2018 Day2, task A')({ + contestId: 'jag2018summer-day2', + taskId: 'jag2018summer_day2_a', + expected: `${ATCODER_BASE_CONTEST_URL}/jag2018summer-day2/tasks/jag2018summer_day2_a`, + }), + createTestCaseForTaskUrl('JAG Summer Camp 2013 Day2 WarmUp, task A')({ + contestId: 'jag2013summer-warmingup', + taskId: 'icpc2013summer_warmingUp_a', + expected: `${ATCODER_BASE_CONTEST_URL}/jag2013summer-warmingup/tasks/icpc2013summer_warmingUp_a`, + }), +]; + const courses = [ { contestId: 'ITP1', From 13a839669dcb8e7fcf223ef9213eb82ac602d35a Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:17:28 +0000 Subject: [PATCH 3/7] feat(tasks): add JAG summer camps 2025/2026 and JAG contests on AtCoder to the seed - Add JAGSummer2025-day1..2026-day3 (AOJ 3648-3719, 72 tasks) after JAGSummer2024-day3 - Add jag2018summer-day2, jag2017summer-day1, jag2015summer-day3 and jag2013summer-warmingup (42 tasks) before the UNIVERSITY block - Extend the seed letter test to 151 contests and 1329 tasks Co-Authored-By: Claude Opus 5.5 --- prisma/tasks.ts | 798 ++++++++++++++++++ .../seed_problem_letters.test.ts | 6 +- 2 files changed, 801 insertions(+), 3 deletions(-) diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 09de7f3ca..8dc228599 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -9481,6 +9481,300 @@ export const tasks = [ name: 'お菓子', title: 'A. お菓子', }, + { + id: 'jag2018summer_day2_k', + contest_id: 'jag2018summer-day2', + problem_index: 'K', + name: 'Short LIS', + title: 'K. Short LIS', + }, + { + id: 'jag2018summer_day2_j', + contest_id: 'jag2018summer-day2', + problem_index: 'J', + name: 'AB Sort', + title: 'J. AB Sort', + }, + { + id: 'jag2018summer_day2_i', + contest_id: 'jag2018summer-day2', + problem_index: 'I', + name: 'ADD DIV MAX RESTORE', + title: 'I. ADD DIV MAX RESTORE', + }, + { + id: 'jag2018summer_day2_h', + contest_id: 'jag2018summer-day2', + problem_index: 'H', + name: 'Prefix Suffix Free', + title: 'H. Prefix Suffix Free', + }, + { + id: 'jag2018summer_day2_g', + contest_id: 'jag2018summer-day2', + problem_index: 'G', + name: 'Construct One Point', + title: 'G. Construct One Point', + }, + { + id: 'jag2018summer_day2_f', + contest_id: 'jag2018summer-day2', + problem_index: 'F', + name: 'Point Sequences', + title: 'F. Point Sequences', + }, + { + id: 'jag2018summer_day2_e', + contest_id: 'jag2018summer-day2', + problem_index: 'E', + name: 'Self-contained', + title: 'E. Self-contained', + }, + { + id: 'jag2018summer_day2_d', + contest_id: 'jag2018summer-day2', + problem_index: 'D', + name: 'Knapsack And Queries', + title: 'D. Knapsack And Queries', + }, + { + id: 'jag2018summer_day2_c', + contest_id: 'jag2018summer-day2', + problem_index: 'C', + name: 'Equiangular', + title: 'C. Equiangular', + }, + { + id: 'jag2018summer_day2_b', + contest_id: 'jag2018summer-day2', + problem_index: 'B', + name: 'Coins', + title: 'B. Coins', + }, + { + id: 'jag2018summer_day2_a', + contest_id: 'jag2018summer-day2', + problem_index: 'A', + name: '10^N+7', + title: 'A. 10^N+7', + }, + { + id: 'jag2017summer_day1_k', + contest_id: 'jag2017summer-day1', + problem_index: 'K', + name: 'パンプキン', + title: 'K. パンプキン', + }, + { + id: 'jag2017summer_day1_j', + contest_id: 'jag2017summer-day1', + problem_index: 'J', + name: 'クッパ', + title: 'J. クッパ', + }, + { + id: 'jag2017summer_day1_i', + contest_id: 'jag2017summer-day1', + problem_index: 'I', + name: 'ルーク', + title: 'I. ルーク', + }, + { + id: 'jag2017summer_day1_h', + contest_id: 'jag2017summer-day1', + problem_index: 'H', + name: 'イベルタル', + title: 'H. イベルタル', + }, + { + id: 'jag2017summer_day1_g', + contest_id: 'jag2017summer-day1', + problem_index: 'G', + name: 'ツーリスト問題', + title: 'G. ツーリスト問題', + }, + { + id: 'jag2017summer_day1_f', + contest_id: 'jag2017summer-day1', + problem_index: 'F', + name: '極小部分列', + title: 'F. 極小部分列', + }, + { + id: 'jag2017summer_day1_e', + contest_id: 'jag2017summer-day1', + problem_index: 'E', + name: 'ベクトル式', + title: 'E. ベクトル式', + }, + { + id: 'jag2017summer_day1_d', + contest_id: 'jag2017summer-day1', + problem_index: 'D', + name: 'くさかべ', + title: 'D. くさかべ', + }, + { + id: 'jag2017summer_day1_c', + contest_id: 'jag2017summer-day1', + problem_index: 'C', + name: 'すごろく', + title: 'C. すごろく', + }, + { + id: 'jag2017summer_day1_b', + contest_id: 'jag2017summer-day1', + problem_index: 'B', + name: 'リス', + title: 'B. リス', + }, + { + id: 'jag2017summer_day1_a', + contest_id: 'jag2017summer-day1', + problem_index: 'A', + name: 'しりとり', + title: 'A. しりとり', + }, + { + id: 'icpc2015summer_day3_j', + contest_id: 'jag2015summer-day3', + problem_index: 'J', + name: 'Jelly-Oxygen Beans', + title: 'J. Jelly-Oxygen Beans', + }, + { + id: 'icpc2015summer_day3_i', + contest_id: 'jag2015summer-day3', + problem_index: 'I', + name: 'Icy Roads Of Nomel', + title: 'I. Icy Roads Of Nomel', + }, + { + id: 'icpc2015summer_day3_h', + contest_id: 'jag2015summer-day3', + problem_index: 'H', + name: 'Handicapped Onsite Prediction', + title: 'H. Handicapped Onsite Prediction', + }, + { + id: 'icpc2015summer_day3_g', + contest_id: 'jag2015summer-day3', + problem_index: 'G', + name: "Gowk's Errand for Master", + title: "G. Gowk's Errand for Master", + }, + { + id: 'icpc2015summer_day3_f', + contest_id: 'jag2015summer-day3', + problem_index: 'F', + name: 'Figures Of Simple Sense', + title: 'F. Figures Of Simple Sense', + }, + { + id: 'icpc2015summer_day3_e', + contest_id: 'jag2015summer-day3', + problem_index: 'E', + name: 'Exact Number of Drops', + title: 'E. Exact Number of Drops', + }, + { + id: 'icpc2015summer_day3_d', + contest_id: 'jag2015summer-day3', + problem_index: 'D', + name: "Degree Of Number's Eccentricity", + title: "D. Degree Of Number's Eccentricity", + }, + { + id: 'icpc2015summer_day3_c', + contest_id: 'jag2015summer-day3', + problem_index: 'C', + name: 'Counting Amusing Numbers', + title: 'C. Counting Amusing Numbers', + }, + { + id: 'icpc2015summer_day3_b', + contest_id: 'jag2015summer-day3', + problem_index: 'B', + name: 'Bits Are Dangerous', + title: 'B. Bits Are Dangerous', + }, + { + id: 'icpc2015summer_day3_a', + contest_id: 'jag2015summer-day3', + problem_index: 'A', + name: 'Analyzing Bit (Yet Special) Strings', + title: 'A. Analyzing Bit (Yet Special) Strings', + }, + { + id: 'icpc2013summer_warmingUp_j', + contest_id: 'jag2013summer-warmingup', + problem_index: 'J', + name: 'Very Intellectual Card Game', + title: 'J. Very Intellectual Card Game', + }, + { + id: 'icpc2013summer_warmingUp_i', + contest_id: 'jag2013summer-warmingup', + problem_index: 'I', + name: 'Topology', + title: 'I. Topology', + }, + { + id: 'icpc2013summer_warmingUp_h', + contest_id: 'jag2013summer-warmingup', + problem_index: 'H', + name: 'Shuffling Machine', + title: 'H. Shuffling Machine', + }, + { + id: 'icpc2013summer_warmingUp_g', + contest_id: 'jag2013summer-warmingup', + problem_index: 'G', + name: 'Moving Points', + title: 'G. Moving Points', + }, + { + id: 'icpc2013summer_warmingUp_f', + contest_id: 'jag2013summer-warmingup', + problem_index: 'F', + name: 'Maximum Segment XOR', + title: 'F. Maximum Segment XOR', + }, + { + id: 'icpc2013summer_warmingUp_e', + contest_id: 'jag2013summer-warmingup', + problem_index: 'E', + name: 'Magic Doors', + title: 'E. Magic Doors', + }, + { + id: 'icpc2013summer_warmingUp_d', + contest_id: 'jag2013summer-warmingup', + problem_index: 'D', + name: 'Graph Destruction', + title: 'D. Graph Destruction', + }, + { + id: 'icpc2013summer_warmingUp_c', + contest_id: 'jag2013summer-warmingup', + problem_index: 'C', + name: 'Containers', + title: 'C. Containers', + }, + { + id: 'icpc2013summer_warmingUp_b', + contest_id: 'jag2013summer-warmingup', + problem_index: 'B', + name: 'Cans of Toys', + title: 'B. Cans of Toys', + }, + { + id: 'icpc2013summer_warmingUp_a', + contest_id: 'jag2013summer-warmingup', + problem_index: 'A', + name: 'Anime Master', + title: 'A. Anime Master', + }, { id: 'bogosortcontest_div1_n', contest_id: 'bogosortcontest_div1', @@ -18167,6 +18461,510 @@ export const tasks = [ name: 'Equal or Not Equal', title: 'Equal or Not Equal', }, + { + id: '3648', + contest_id: 'JAGSummer2025-day1', + problem_index: 'A', + name: 'Dyad', + title: 'Dyad', + }, + { + id: '3649', + contest_id: 'JAGSummer2025-day1', + problem_index: 'B', + name: 'JAG Box', + title: 'JAG Box', + }, + { + id: '3650', + contest_id: 'JAGSummer2025-day1', + problem_index: 'C', + name: 'Chairs', + title: 'Chairs', + }, + { + id: '3651', + contest_id: 'JAGSummer2025-day1', + problem_index: 'D', + name: 'Inversion of Suffix Array', + title: 'Inversion of Suffix Array', + }, + { + id: '3652', + contest_id: 'JAGSummer2025-day1', + problem_index: 'E', + name: 'Pole', + title: 'Pole', + }, + { + id: '3653', + contest_id: 'JAGSummer2025-day1', + problem_index: 'F', + name: 'Walking on Binary Tree', + title: 'Walking on Binary Tree', + }, + { + id: '3654', + contest_id: 'JAGSummer2025-day1', + problem_index: 'G', + name: 'Path Flipping', + title: 'Path Flipping', + }, + { + id: '3655', + contest_id: 'JAGSummer2025-day1', + problem_index: 'H', + name: 'Colored Tree and Path', + title: 'Colored Tree and Path', + }, + { + id: '3656', + contest_id: 'JAGSummer2025-day1', + problem_index: 'I', + name: 'Inside Yamanote', + title: 'Inside Yamanote', + }, + { + id: '3657', + contest_id: 'JAGSummer2025-day1', + problem_index: 'J', + name: 'Billion Tree', + title: 'Billion Tree', + }, + { + id: '3658', + contest_id: 'JAGSummer2025-day1', + problem_index: 'K', + name: 'LOGCFL', + title: 'LOGCFL', + }, + { + id: '3659', + contest_id: 'JAGSummer2025-day1', + problem_index: 'L', + name: 'Sum of Floor(N/ij)', + title: 'Sum of Floor(N/ij)', + }, + { + id: '3660', + contest_id: 'JAGSummer2025-day2', + problem_index: 'A', + name: 'Mix Condiments', + title: 'Mix Condiments', + }, + { + id: '3661', + contest_id: 'JAGSummer2025-day2', + problem_index: 'B', + name: 'Broken Keyboard', + title: 'Broken Keyboard', + }, + { + id: '3662', + contest_id: 'JAGSummer2025-day2', + problem_index: 'C', + name: 'Triangles', + title: 'Triangles', + }, + { + id: '3663', + contest_id: 'JAGSummer2025-day2', + problem_index: 'D', + name: 'Strange House', + title: 'Strange House', + }, + { + id: '3664', + contest_id: 'JAGSummer2025-day2', + problem_index: 'E', + name: 'To All Tha Customers', + title: 'To All Tha Customers', + }, + { + id: '3665', + contest_id: 'JAGSummer2025-day2', + problem_index: 'F', + name: '00 to 1', + title: '00 to 1', + }, + { + id: '3666', + contest_id: 'JAGSummer2025-day2', + problem_index: 'G', + name: 'Driving Playlist', + title: 'Driving Playlist', + }, + { + id: '3667', + contest_id: 'JAGSummer2025-day2', + problem_index: 'H', + name: 'Count Unique Packing', + title: 'Count Unique Packing', + }, + { + id: '3668', + contest_id: 'JAGSummer2025-day2', + problem_index: 'I', + name: 'Double 01 on Tree', + title: 'Double 01 on Tree', + }, + { + id: '3669', + contest_id: 'JAGSummer2025-day2', + problem_index: 'J', + name: "Hanako's Art II", + title: "Hanako's Art II", + }, + { + id: '3670', + contest_id: 'JAGSummer2025-day2', + problem_index: 'K', + name: 'All Copy Paste', + title: 'All Copy Paste', + }, + { + id: '3671', + contest_id: 'JAGSummer2025-day2', + problem_index: 'L', + name: 'Arrange One More Office', + title: 'Arrange One More Office', + }, + { + id: '3672', + contest_id: 'JAGSummer2025-day3', + problem_index: 'A', + name: 'Spray on Surface', + title: 'Spray on Surface', + }, + { + id: '3673', + contest_id: 'JAGSummer2025-day3', + problem_index: 'B', + name: 'Funini Adventure', + title: 'Funini Adventure', + }, + { + id: '3674', + contest_id: 'JAGSummer2025-day3', + problem_index: 'C', + name: 'Communication between islands', + title: 'Communication between islands', + }, + { + id: '3675', + contest_id: 'JAGSummer2025-day3', + problem_index: 'D', + name: 'Grid Painting', + title: 'Grid Painting', + }, + { + id: '3676', + contest_id: 'JAGSummer2025-day3', + problem_index: 'E', + name: 'Word Search', + title: 'Word Search', + }, + { + id: '3677', + contest_id: 'JAGSummer2025-day3', + problem_index: 'F', + name: 'Ants Collision', + title: 'Ants Collision', + }, + { + id: '3678', + contest_id: 'JAGSummer2025-day3', + problem_index: 'G', + name: 'Fireworks', + title: 'Fireworks', + }, + { + id: '3679', + contest_id: 'JAGSummer2025-day3', + problem_index: 'H', + name: 'Max of Mod', + title: 'Max of Mod', + }, + { + id: '3680', + contest_id: 'JAGSummer2025-day3', + problem_index: 'I', + name: 'Copy, Reflect, and Paste', + title: 'Copy, Reflect, and Paste', + }, + { + id: '3681', + contest_id: 'JAGSummer2025-day3', + problem_index: 'J', + name: 'Flight Planning', + title: 'Flight Planning', + }, + { + id: '3682', + contest_id: 'JAGSummer2025-day3', + problem_index: 'K', + name: 'Bomb Game', + title: 'Bomb Game', + }, + { + id: '3683', + contest_id: 'JAGSummer2025-day3', + problem_index: 'L', + name: 'Broken Scale', + title: 'Broken Scale', + }, + { + id: '3684', + contest_id: 'JAGSummer2026-day1', + problem_index: 'A', + name: 'Maximum Diameter', + title: 'Maximum Diameter', + }, + { + id: '3685', + contest_id: 'JAGSummer2026-day1', + problem_index: 'B', + name: 'XOR Cycle', + title: 'XOR Cycle', + }, + { + id: '3686', + contest_id: 'JAGSummer2026-day1', + problem_index: 'C', + name: 'Neighbour or Opposite', + title: 'Neighbour or Opposite', + }, + { + id: '3687', + contest_id: 'JAGSummer2026-day1', + problem_index: 'D', + name: 'CPK! Remix', + title: 'CPK! Remix', + }, + { + id: '3688', + contest_id: 'JAGSummer2026-day1', + problem_index: 'E', + name: 'JAG City Marathon I', + title: 'JAG City Marathon I', + }, + { + id: '3689', + contest_id: 'JAGSummer2026-day1', + problem_index: 'F', + name: "Professor JAG's Language", + title: "Professor JAG's Language", + }, + { + id: '3690', + contest_id: 'JAGSummer2026-day1', + problem_index: 'G', + name: 'Find Iotas', + title: 'Find Iotas', + }, + { + id: '3691', + contest_id: 'JAGSummer2026-day1', + problem_index: 'H', + name: 'Longest Symmetry', + title: 'Longest Symmetry', + }, + { + id: '3692', + contest_id: 'JAGSummer2026-day1', + problem_index: 'I', + name: 'Capybara Petting Club', + title: 'Capybara Petting Club', + }, + { + id: '3693', + contest_id: 'JAGSummer2026-day1', + problem_index: 'J', + name: 'Sliding Puzzle on Tree', + title: 'Sliding Puzzle on Tree', + }, + { + id: '3694', + contest_id: 'JAGSummer2026-day1', + problem_index: 'K', + name: 'Group Strength', + title: 'Group Strength', + }, + { + id: '3695', + contest_id: 'JAGSummer2026-day1', + problem_index: 'L', + name: 'Rabbit, Rabbit, Rabbit', + title: 'Rabbit, Rabbit, Rabbit', + }, + { + id: '3696', + contest_id: 'JAGSummer2026-day2', + problem_index: 'A', + name: 'Brace for Impact!', + title: 'Brace for Impact!', + }, + { + id: '3697', + contest_id: 'JAGSummer2026-day2', + problem_index: 'B', + name: 'Deque Bracket Optimization', + title: 'Deque Bracket Optimization', + }, + { + id: '3698', + contest_id: 'JAGSummer2026-day2', + problem_index: 'C', + name: 'Two Greedy Customers', + title: 'Two Greedy Customers', + }, + { + id: '3699', + contest_id: 'JAGSummer2026-day2', + problem_index: 'D', + name: 'Robot Damage', + title: 'Robot Damage', + }, + { + id: '3700', + contest_id: 'JAGSummer2026-day2', + problem_index: 'E', + name: 'Vibe Coding', + title: 'Vibe Coding', + }, + { + id: '3701', + contest_id: 'JAGSummer2026-day2', + problem_index: 'F', + name: 'Tree + 1', + title: 'Tree + 1', + }, + { + id: '3702', + contest_id: 'JAGSummer2026-day2', + problem_index: 'G', + name: 'Longest Path on DAG', + title: 'Longest Path on DAG', + }, + { + id: '3703', + contest_id: 'JAGSummer2026-day2', + problem_index: 'H', + name: 'Warp', + title: 'Warp', + }, + { + id: '3704', + contest_id: 'JAGSummer2026-day2', + problem_index: 'I', + name: 'Buttons', + title: 'Buttons', + }, + { + id: '3705', + contest_id: 'JAGSummer2026-day2', + problem_index: 'J', + name: 'Take Many ABA', + title: 'Take Many ABA', + }, + { + id: '3706', + contest_id: 'JAGSummer2026-day2', + problem_index: 'K', + name: 'Empty Intersection', + title: 'Empty Intersection', + }, + { + id: '3707', + contest_id: 'JAGSummer2026-day2', + problem_index: 'L', + name: 'Edge Exchanges', + title: 'Edge Exchanges', + }, + { + id: '3708', + contest_id: 'JAGSummer2026-day3', + problem_index: 'A', + name: 'Ants Sort', + title: 'Ants Sort', + }, + { + id: '3709', + contest_id: 'JAGSummer2026-day3', + problem_index: 'B', + name: 'ABbreviation', + title: 'ABbreviation', + }, + { + id: '3710', + contest_id: 'JAGSummer2026-day3', + problem_index: 'C', + name: 'Line Up!', + title: 'Line Up!', + }, + { + id: '3711', + contest_id: 'JAGSummer2026-day3', + problem_index: 'D', + name: 'Self-Destruct Sequence', + title: 'Self-Destruct Sequence', + }, + { + id: '3712', + contest_id: 'JAGSummer2026-day3', + problem_index: 'E', + name: 'Box Tower II', + title: 'Box Tower II', + }, + { + id: '3713', + contest_id: 'JAGSummer2026-day3', + problem_index: 'F', + name: 'vs Hydra', + title: 'vs Hydra', + }, + { + id: '3714', + contest_id: 'JAGSummer2026-day3', + problem_index: 'G', + name: 'Guess Palindrome', + title: 'Guess Palindrome', + }, + { + id: '3715', + contest_id: 'JAGSummer2026-day3', + problem_index: 'H', + name: 'Noise Cancelling', + title: 'Noise Cancelling', + }, + { + id: '3716', + contest_id: 'JAGSummer2026-day3', + problem_index: 'I', + name: 'JAG City Marathon II', + title: 'JAG City Marathon II', + }, + { + id: '3717', + contest_id: 'JAGSummer2026-day3', + problem_index: 'J', + name: 'Forbidden Word', + title: 'Forbidden Word', + }, + { + id: '3718', + contest_id: 'JAGSummer2026-day3', + problem_index: 'K', + name: 'Two Friend Groups', + title: 'Two Friend Groups', + }, + { + id: '3719', + contest_id: 'JAGSummer2026-day3', + problem_index: 'L', + name: 'Treasure Hunting', + title: 'Treasure Hunting', + }, { id: '2073', contest_id: 'JAGWinter2006-day2', diff --git a/src/lib/clients/aizu_online_judge/seed_problem_letters.test.ts b/src/lib/clients/aizu_online_judge/seed_problem_letters.test.ts index ae85a8e96..a98c7eb03 100644 --- a/src/lib/clients/aizu_online_judge/seed_problem_letters.test.ts +++ b/src/lib/clients/aizu_online_judge/seed_problem_letters.test.ts @@ -89,14 +89,14 @@ describe('problem_index of AOJ ICPC / JAG tasks in prisma/tasks.ts', () => { const indicesByContest = groupProblemIndicesByContest(); // Guards against the filter silently matching fewer tasks. - test('covers 145 contests and 1257 tasks', () => { + test('covers 151 contests and 1329 tasks', () => { const taskCount = [...indicesByContest.values()].reduce( (sum, indices) => sum + indices.size, 0, ); - expect(indicesByContest.size).toBe(145); - expect(taskCount).toBe(1257); + expect(indicesByContest.size).toBe(151); + expect(taskCount).toBe(1329); }); test.each( From 43f7265d6e15a8cbc75236ed70af394fdd2b2bd0 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:23:13 +0000 Subject: [PATCH 4/7] test(contests): pin ATCODER_JAG to exact contest_id matches - Reject contest_ids that extend a listed JAG contest held on AtCoder, so a switch to prefix matching on the listed keys is caught - List ATCODER_JAG among the exact-match maps in the prefixes.ts header Addresses Codex cross-review findings on PR2.1. Co-Authored-By: Claude Opus 5.5 --- src/lib/contests/utils/classification.test.ts | 10 ++++++++++ src/lib/contests/utils/prefixes.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/contests/utils/classification.test.ts b/src/lib/contests/utils/classification.test.ts index 7f45b35f4..97af958f6 100644 --- a/src/lib/contests/utils/classification.test.ts +++ b/src/lib/contests/utils/classification.test.ts @@ -191,6 +191,16 @@ describe('classify contest', () => { }); }); + // Matched exactly, so a new JAG contest on AtCoder must be listed explicitly. + describe('when contest_id extends a listed JAG contest held on AtCoder', () => { + test.each(['jag2018summer-day2-extra', 'jag2017summer-day1b'])( + 'returns null for %s', + (contestId) => { + expect(classifyContest(contestId)).toBeNull(); + }, + ); + }); + describe('when contest_id matches contests held by university students', () => { TestCasesForContestType.universities.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { diff --git a/src/lib/contests/utils/prefixes.ts b/src/lib/contests/utils/prefixes.ts index 3a3590dca..393f19358 100644 --- a/src/lib/contests/utils/prefixes.ts +++ b/src/lib/contests/utils/prefixes.ts @@ -5,7 +5,7 @@ export const regexForAojUniversity = /^AOJ-[A-Z]+PC\d{4}/; // HACK: As of October 2026, the following contests are applicable. // Note: The classification logic may need to be revised when new contests are added. -// ABC_LIKE / ARC_LIKE / ATCODER_MAIN_OFFICIAL_ONSITE: exact match — add sequels (e.g. diverta2019-2) explicitly. Others: prefix match. +// ABC_LIKE / ARC_LIKE / ATCODER_JAG / ATCODER_MAIN_OFFICIAL_ONSITE: exact match — add sequels (e.g. diverta2019-2) explicitly. Others: prefix match. export const ABC_LIKE: ContestPrefix = { 'tenka1-2017-beginner': 'Tenka1 Programmer Beginner Contest 2017', abl: 'ACL Beginner Contest', From 62bdefcd120064bce8717ef5eddabb5e32782253 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:24:13 +0000 Subject: [PATCH 5/7] docs: record the PR2.1 implementation and the Codex review outcome Co-Authored-By: Claude Opus 5.5 --- .../plan-pr2-1.md | 29 +++++++++++++++++-- .../task-title-index-change-impact/plan.md | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md index 391e41d39..4be3c1fca 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md @@ -2,7 +2,7 @@ 前提・共通の決定・データ移行の手順は [plan.md](./plan.md)。スプレッドシートとの対応は [investigation.md](./investigation.md#db-に無いコンテスト)。 -**状態**: 未着手。必要な情報(問題番号・contest_id・CSV の作り方)は 2026-10-24 にメンテナから受け取った。PR2 / PR2.5 と前後してよい。 +**状態**: 2.1-0〜2.1-2・2.1-4 は実装済み(#4221、Codex のレビュー対応まで。[実装の記録](#実装の記録))。残りは、メンテナによるローカルでの AtCoder 取り込みの確認と、staging / 本番への投入(2.1-3)。必要な情報(問題番号・contest_id・CSV の作り方)は 2026-10-24 にメンテナから受け取った。PR2 / PR2.5 と前後してよい。 ## 範囲 @@ -56,7 +56,7 @@ ラベルに(有志)は付けない。PR2.5 のテーブル名でだけ、2018 Day2 と 2017 Day1 に `(有志)` を付ける(M16)。 -- 優先度をずらすので、`src/test/lib/utils/task.test.ts` の優先度の差の期待値のうち、ずれた項目を含むものを直す +- 優先度をずらしても、`src/test/lib/utils/task.test.ts` の優先度の差の期待値は直さずに済んだ(0〜11 の項目しか含まない) - 4 つの contest_id は小文字の `jag…` で始まるので、`regexForJag`(大文字の `JAG(Prelim|…)`)にも `AojGenerator` の `startsWith('ICPC')` にも当たらない。`getTaskUrl` は AtCoder の URL を作る 却下した方針: @@ -83,3 +83,28 @@ | 移行の前に、既存の合宿系と同じ形(問題 ID のまま)で入れる | PR2 の移行 SQL の件数の想定値が、投入の有無で変わる。記号付きで入れれば移行と独立する | | 管理画面の取り込みに合宿系を足す | 合宿系も AOJ API にある(JAG の filter に summer / winter / spring がある)。ただし既存の contest_id(`JAGSummer2012-day3A` など)にそろえるアドホックな対応が多く、年 1 回の追加に対して重い。**応答の形は意図的に確かめていない**(メンテナの判断、2026-10-24) | | `id` を取り込みと同じ規則にそろえる | 既存の合宿系と規則が分かれる。`id` を再計算する箇所が無いので、そろえる利点が無い | + +## 実装の記録 + +コミット(#4221): `524b800a`(`ATCODER_JAG` の新設)、`13a83966`(seed)、`43f7265d`(レビュー対応)。 + +- seed の AOJ 72 行は `JAGSummer2024-day3` の直後に、古いコンテストから・問題 ID の昇順で置いた。add-tasks の規則(新しい順・降順)は AtCoder 側の慣習で、既存の AOJ のブロックはすべて昇順なので、近傍にそろえた +- seed の AtCoder 42 行は、UNIVERSITY のブロック(`bogosortcontest_div1`)の直前に、新しいコンテストから・記号の降順で置いた +- enum の値はマイグレーション(`ADD VALUE`)で DB 上の末尾に足されるので、schema.prisma の並びとは違う。アプリは優先度を `contestTypePriorities` で決め、`contest_type` で並べるクエリも無いので影響しない +- 変異テストで、seed(記号の入れ替え・行の欠落・title の不一致)と `ATCODER_JAG`(map の欠落・優先度の入れ替え・前方一致への緩和・ラベルの定数化)をすべて検出することを確かめた +- ローカルで AtCoder の取り込みを確かめるときは、`pnpm db:seed` より先に行う。seed にも 42 問が入っているので、後だと取り込みが skip される + +### 本番で気をつけること + +- AtCoder の 4 コンテストの取り込みは、本番のデプロイが終わってから行う +- 取り込みの後に PR2.1 より前の版へロールバックすると、旧版の Prisma Client は `ATCODER_JAG` を知らないので、該当行を読む処理が失敗しうる。ロールバックする前に、該当行の `contest_type` を旧版にある値(例: `OTHERS`)に変える + +### クロスレビュー(Codex) + +| # | 指摘 | 対応 | +| --- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| 1 | AOJ の 72 行の並び順が add-tasks の規則(新しい順・降順)に反する | 対応しない。既存の AOJ のブロックにそろえた(上記) | +| 2 | 基本ルールの検査が、期待値を本番の `assignProblemLetters` で作っている | 対応しない。PR1 以来の設計。この関数にはリテラルの期待値の単体テストがあり、seed の値はこの関数から作っていないので誤りを共有しない | +| 3 | `prefixes.ts` 冒頭のコメントで、`ATCODER_JAG` が前方一致の側に入っている | 直した | +| 4 | 似ているが違う contest_id を弾くテストが無い | 足した。4 つのキーでの前方一致に変えると検出される | +| 5 | 旧版のアプリと新しい enum 値が重なると読めない | コードは変えず、[本番で気をつけること](#本番で気をつけること)に書いた | diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md index dcf0899bd..fd058e5ee 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan.md @@ -1,6 +1,6 @@ # AOJ ICPC / JAG の `problem_index` を A/B/C 形式へ変更する -**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)は staging に反映、本番は未反映 +**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)は staging に反映、本番は未反映。PR2.1(#4221)は実装済み(PR 前) このファイルは全体像と、PR をまたぐ決定・手順だけを書く。各 PR の作業はそれぞれのファイルにある。 From d37be2c458db2efee4935734cc95504e23c9a6b4 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:31:25 +0000 Subject: [PATCH 6/7] docs: record the local AtCoder import check for PR2.1 Co-Authored-By: Claude Opus 5.5 --- .../2026-09-09/task-title-index-change-impact/plan-pr2-1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md index 4be3c1fca..20a34e3a5 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md @@ -2,7 +2,7 @@ 前提・共通の決定・データ移行の手順は [plan.md](./plan.md)。スプレッドシートとの対応は [investigation.md](./investigation.md#db-に無いコンテスト)。 -**状態**: 2.1-0〜2.1-2・2.1-4 は実装済み(#4221、Codex のレビュー対応まで。[実装の記録](#実装の記録))。残りは、メンテナによるローカルでの AtCoder 取り込みの確認と、staging / 本番への投入(2.1-3)。必要な情報(問題番号・contest_id・CSV の作り方)は 2026-10-24 にメンテナから受け取った。PR2 / PR2.5 と前後してよい。 +**状態**: 2.1-0〜2.1-2・2.1-4 は実装済み(#4221、Codex のレビュー対応まで。[実装の記録](#実装の記録))。ローカルでの AtCoder 取り込みは 2026-10-25 にメンテナが確かめた。残りは staging / 本番への投入(2.1-3)。必要な情報(問題番号・contest_id・CSV の作り方)は 2026-10-24 にメンテナから受け取った。PR2 / PR2.5 と前後してよい。 ## 範囲 @@ -93,6 +93,7 @@ - enum の値はマイグレーション(`ADD VALUE`)で DB 上の末尾に足されるので、schema.prisma の並びとは違う。アプリは優先度を `contestTypePriorities` で決め、`contest_type` で並べるクエリも無いので影響しない - 変異テストで、seed(記号の入れ替え・行の欠落・title の不一致)と `ATCODER_JAG`(map の欠落・優先度の入れ替え・前方一致への緩和・ラベルの定数化)をすべて検出することを確かめた - ローカルで AtCoder の取り込みを確かめるときは、`pnpm db:seed` より先に行う。seed にも 42 問が入っているので、後だと取り込みが skip される +- ローカルの取り込みで入った 42 行は、task_id・contest_id・記号・title のすべてが seed と一致した。開発サーバーを enum の追加より前から動かしていると、古い Prisma Client が `ATCODER_JAG` を受け付けず、登録が 500 になる(再起動で直る) ### 本番で気をつけること From 96207156fad5eacddde2a8c8c4716607cdd39f00 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 03:49:44 +0000 Subject: [PATCH 7/7] docs: clarify ATCODER_JAG rollback recovery procedure --- .../plan-pr2-1.md | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md index 20a34e3a5..d19b66221 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md @@ -98,7 +98,53 @@ ### 本番で気をつけること - AtCoder の 4 コンテストの取り込みは、本番のデプロイが終わってから行う -- 取り込みの後に PR2.1 より前の版へロールバックすると、旧版の Prisma Client は `ATCODER_JAG` を知らないので、該当行を読む処理が失敗しうる。ロールバックする前に、該当行の `contest_type` を旧版にある値(例: `OTHERS`)に変える +- 取り込みの後に PR2.1 より前の版へロールバックすると、旧版の Prisma Client は `ATCODER_JAG` を知らないので、該当行を読む処理が失敗しうる。ロールバックする前に、該当行の `contest_type` を `OTHERS` に変える +- その後に PR2.1 以降へ戻したら(ロールフォワード)、下の SQL で該当行を `ATCODER_JAG` に戻す。取り込みは既存の task_id を skip するので、取り込み直しても `OTHERS` のまま残る。いまは DB の `contest_type` を読む箇所が無いので表示は壊れないが、分類と DB の値がずれたままになる + +#### ロールバックとロールフォワードの復旧手順 + +1. ロールバック前に、対象環境で次の SQL を実行し、返された `expected_count` を環境名とともに作業記録へ保存する。これは実際に `OTHERS` へ変更した件数で、全問を取り込んでいれば 42、一部だけならその件数になる。以降、復旧の確認が終わるまでは対象 4 コンテストの追加・削除・分類変更を行わない。 + +```sql +WITH changed AS ( + UPDATE "task" + SET "contest_type" = 'OTHERS' + WHERE "contest_id" IN ('jag2018summer-day2', 'jag2017summer-day1', 'jag2015summer-day3', 'jag2013summer-warmingup') + AND "contest_type" = 'ATCODER_JAG' + RETURNING "id" +) +SELECT count(*) AS expected_count FROM changed; +``` + +2. SQL の成功と件数の保存を確認してから旧版へロールバックする。復旧時は、先に PR2.1 以降をデプロイし、新しい Prisma Client が動作する状態にする。 +3. 次の `expected_count` の 42 を、手順 1 で保存した件数に置き換えて実行する。復旧直前の行数を期待値にするのではなく、ロールバック時の記録と照合する。 + +```sql +DO $$ +DECLARE + expected_count integer := 42; -- Replace with the recorded rollback count. + restored_count integer; +BEGIN + UPDATE "task" + SET "contest_type" = 'ATCODER_JAG' + WHERE "contest_id" IN ('jag2018summer-day2', 'jag2017summer-day1', 'jag2015summer-day3', 'jag2013summer-warmingup') + AND "contest_type" = 'OTHERS'; + + GET DIAGNOSTICS restored_count = ROW_COUNT; + IF restored_count <> expected_count THEN + RAISE EXCEPTION 'restored % rows, expected %', restored_count, expected_count; + END IF; + RAISE NOTICE 'restored % rows, expected %', restored_count, expected_count; +END $$; + +-- Expected: ATCODER_JAG only, with the recorded rollback count. +SELECT "contest_type", count(*) FROM "task" +WHERE "contest_id" IN ('jag2018summer-day2', 'jag2017summer-day1', 'jag2015summer-day3', 'jag2013summer-warmingup') +GROUP BY "contest_type"; +``` + +4. `GET DIAGNOSTICS` で取得した実更新件数と期待件数が一致したことを NOTICE で確認する。不一致なら例外で DO ブロック内の UPDATE 全体が取り消されるため、期待件数を書き換えて通すのではなく、作業記録と対象行を調べる。 +5. 最後の SELECT が `ATCODER_JAG` の 1 行だけを返し、その件数が手順 1 の記録と一致すれば復旧完了とする(全問なら `ATCODER_JAG | 42`)。記録が 0 件の場合は、SELECT も 0 行であることを確認する。`OTHERS` など別の分類が残る場合は復旧未完了として調査する。 ### クロスレビュー(Codex)