From 1f0bb91c889e97b24a9022e4db5694ea0825bf98 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 05:15:37 +0000 Subject: [PATCH 1/5] docs: fold the PR2.1 plan into the decision summary Co-Authored-By: Claude Sonnet 5.5 --- .../task-title-index-change-impact/done.md | 292 +++++++++++------- .../investigation.md | 2 +- .../plan-pr2-1.md | 157 ---------- .../plan-pr2-5.md | 2 +- .../task-title-index-change-impact/plan.md | 25 +- 5 files changed, 199 insertions(+), 279 deletions(-) delete mode 100644 docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.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 5fd6aa615..e382d8ef2 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,120 +1,196 @@ -# 完了分(PR0 / PR1 / PR1.5 / PR2)— 意思決定の要約 +# 完了分(PR0〜PR2.1)— 意思決定の要約 -**状態**: PR0 完了(#4212)・PR1 完了(#4217)・PR1.5 完了(#4219)・PR2 は staging に反映(#4220、本番は未反映)。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 +コードと git の履歴から読み取れない判断と事実だけを、項目ごとにまとめる。変更したファイルや関数は各 PR の差分を見る。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 -コードと git の履歴から読み取れない判断と事実だけを残す。変更したファイルや関数は #4212 / #4217 / #4219 / #4220 の差分を見る。PR1.5 は [こちら](#pr15--aoj-api-の移転に追従する4028)、PR2 は [こちら](#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030)。 +| PR | 内容 | 状態 | +| -------------- | --------------------------------------------------------------------- | -------------------------------------- | +| PR0(#4212) | 出典欄の AOJ の書式 | 完了 | +| PR1(#4217) | ICPC 国内予選 / アジア地区 / JAG 模擬国内(79 / 632)を問題記号にする | 完了 | +| PR1.5(#4219) | AOJ API の移転に追従する(#4028) | 完了 | +| PR2(#4220) | JAG 合宿系と JAG 模擬地区(66 / 625)を問題記号にする(#4030) | staging に反映、本番は未反映 | +| PR2.1(#4222) | DB に無い 10 コンテストを入れる(#4221) | staging に反映、データの投入と本番は未 | -## 何をしたか +## 残っている作業 -- **PR0**: 出典欄の AOJ 分岐を `${contestName} - ${taskTableIndex}(AOJ)` にし、AOJ のコンテスト名ラベルから全角括弧を外した -- **PR1**: ICPC 国内予選 / アジア地区 / JAG 模擬国内(79 コンテスト / 632 問)の `task_table_index` を問題記号にした。移行 SQL は `prisma/migrations/20261024033928_update_aoj_task_table_index/`。管理画面の取り込みでも、この範囲は基本ルールで記号を付ける。表示側の変換(`buildAojLetterMap` / `AOJ_LABEL_OVERRIDES` / `formatAojTitle` / `getTaskLabels`)は削除した +### 本番・staging での確認 + +- PR2: 本番(`main` への昇格後)で、移行が通ること +- PR2: 問題一覧の出典欄の表示(例: `JAG 夏合宿 2024 Day3 - H(AOJ)`) +- PR2.1: staging で、管理画面から AtCoder の 4 コンテストを取り込み、2025 / 2026 夏合宿の CSV を投入する(PR2 の移行は適用済み) +- PR2.1: 本番(`main` への昇格後)で、デプロイが終わってから 4 コンテストを取り込み、PR2 の移行が通った後に CSV を投入する +- PR2.1 の CSV には `updated_at` を含める(`@updatedAt` は Prisma 側の実装で、DB に既定値が無い。`created_at` / `grade` / `atcoder_problems_difficulty` には既定値がある)。投入は `invalidateTaskCaches()` を経ないので、反映はキャッシュの TTL(1 時間)の後になる +- PR1: コンテストテーブルのスクリーンショットでの目視確認 -## 決定と理由 - -| 決定 | 理由 | -| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 移行 SQL は対応表を焼き込まず、基本ルールを SQL で表す | 本番のタスクは管理画面の取り込みで入っており、seed と一致する保証が無い。`VALUES` で書くと本番にしか無い行が残り、`UPDATE` は該当行が無くてもエラーにならないので、取りこぼしが静かに起きる | -| 範囲は `contest_type` ではなく `contest_id` の接頭辞で絞る | 合宿系も模擬国内と同じ `AOJ_JAG` に分類される。`contest_type` だけで絞ると合宿系 467 件まで更新し、title のプレフィックスが表に出る | -| 取り込みでは、対応表ではなく基本ルールで記号を付ける(M5) | 対応表を AOJ client に置く案は、毎年の新しいコンテストで追記が要り、忘れると数値の index が黙って入る。`prisma/tasks.ts` と同じ情報を 2 か所に持つことにもなる | -| 表示側の変換と `getTaskLabels` を削除する(M7 / M10) | DB が記号を持てば変換は恒等になる。インターフェイスの `getTaskLabels` だけを残す案は、上書きする Provider が 0 になり使われないコードが残るので採らなかった | -| ヘッダーの並びは `localeCompare`(M4) | 記号に `Number()` を当てると NaN になり、ソートが効かなくなる | -| AOJ ラベルから全角括弧を外す(M11) | 出典欄の目標書式ではコンテスト名を括弧で囲まない。ラベルは検索・投票・タグ・取り込み画面・提出状況の更新モーダルでも使われており、その表示も変わるが許容した | -| `buildTitleWithTaskIndex` は「外してから付ける」 | AtCoder 側の title は `A. …` を含むので、付けるだけだと二重になる。title にプレフィックスの無い AtCoder の問題は `abs` の practice_1 だけで(本番も同じ)、ABS は index を表示しない | -| `removeTaskIndexFromTitle` は先頭のプレフィックスだけを消す(レビュー) | 記号は title の本文にも現れうる。以前は index が空のとき `Dr. Podboq` → `DrPodboq` のように本文の `. ` まで消していた | -| `AOJ-KUPC2011` の 2279 は title だけを素にし、index(`I`)は触らない | 本番でも index は既に正しい。AOJ に 1 問しか無いので、大学系に範囲を広げるときは基本ルール(`A` になる)の対象から外す | -| PR0 → PR1 → 合宿系の順にする | PR0 は DB に依存しない。PR1 は出典欄の成果がすぐ見え、欠落が少ないので対応表の形式を楽なケースで決められる。逆順はレビュー負荷が低いが、出典欄の反映が遅れ、対応表の形式を最も判断の重い合宿系で作ることになる | - -## 検証で分かったこと - -- **照合**: メンテナのスプレッドシートを 2026-10-21 のスクリーンショットから書き写し、632 問すべてが基本ルールの記号の位置で一致した(補正なし)。ICPC 国内予選 2002 の E(Enclosing Circles)は AOJ に 1123 として存在するが、コンテスト一覧に無いので取り込まれていない(末尾なので記号はずれない。PR3 の対象) -- **照合スクリプト**: `git show 171bf3c9:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_problem_letters.ts`。`prisma/tasks.ts` を import し、件数が既知の値と違えば止まる。title は大文字小文字・空白・記号を無視して比べ、セルで切れた title は前方一致にする。わざと誤りを入れた 5 種類(順番の入れ替え、途中の title の誤り、途中の欠番、問題数の不足、切れた title の前半の誤り)をすべて検出した。スプレッドシート自体の誤りは検出できない -- **SQL**: `row_number()` は `bigint` を返し、`chr(bigint)` は存在しないので `::int` が要る。`EXPLAIN` は `task` の Seq Scan 2 回 + Hash Join(`contest_type` に索引が無いため想定どおり) -- **トランザクション**: マイグレーションはファイル単位で原子的に適用される(3 文目が失敗するマイグレーションで、1–2 文目も残らないことをローカルで確認) -- **表示の回帰**: 旧コードで適用前のデータを、新コードで適用後のデータを描画し、79 コンテストすべてで列順とセルの文字列が一致した。スクリーンショットでの目視は未実施 -- **テストの検出力**: `verify-test-strength` で 15 個の変異をすべて検出した。seed 整合テストの `ICPC Prelim 2024 F is problem 1677` は単独で検出する変異が無いが、唯一の literal な期待値(対応表とデータが一緒にずれたときの歯止め)なので残した -- **2 日開催の JAG 模擬国内**: AOJ API には `JAGPrelim20162`(2738–2751)のように 2 日を 1 コンテストにしたものがある。取り込みは `contest_id` 単位で振るので A–N になる。seed と本番は `JAGPrelim2016A` / `B` に分かれており、取り込みは既存の問題を skip するので影響しない - -## 残タスク +### 残タスク PR2 以降の計画には含めていない。 - 2016 年の JAG 模擬国内を API から取り込むと、記号が seed と食い違う(Codex / CodeRabbit)。直すなら、取り込みで 2 回開催を `A` / `B` の `contest_id` に分け、それぞれ A から振る。`getContests` 側もそろえる -- seed 整合テストの期待値を `assignProblemLetters` で作っている(Codex)。testing.md の「期待値は literal」に合わない。PR2 では補正したコンテストだけを literal にする(M18)。全件を literal にするかは未判断 +- seed 整合テストの期待値を `assignProblemLetters` で作っている(PR1 と PR2.1 の Codex)。testing.md の「期待値は literal」に合わない。PR2 では補正したコンテストだけを literal にする(M18)。全件を literal にするかは未判断 - `src/lib/services/tasks.ts` の `title.replace(task_table_index, …)` は、AOJ の問題に ContestTaskPair を作るとタイトル本文を置換する(Codex)。AOJ の pair は現在 0 件。直すときは先頭の `${index}. ` だけを置換し、`Amidakuji` のような本文と `A. Title` の両方を回帰テストに含める -- コンテストテーブルのスクリーンショットでの目視確認 - ---- - -## PR1.5 — AOJ API の移転に追従する(#4028) - -### 何をしたか - -- `AOJ_API_BASE_URL` を `https://judgeapi.u-aizu.ac.jp/` から `https://onlinejudge.u-aizu.ac.jp/api/` に変えた。旧ホストは 410 を返し、管理画面からの AOJ の取り込み(Courses / PCK / JAG / ICPC)がすべて失敗していた -- `clients.test.ts` のクライアントを本番の定数から作り、nock で受ける URL(`/api/…`)を期待値として直書きした。`nock.disableNetConnect()` も足した -- 開発コンテナの外向き通信の許可リスト(`.devcontainer/init-firewall.sh`)の `judgeapi.u-aizu.ac.jp` を `onlinejudge.u-aizu.ac.jp` に置き換えた - -### 決定と理由 - -| 決定 | 理由 | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 変えるのはベース URL だけ | AOJ Developers の告知(2026-09-07 に Main API が移転)で変わったのはホストだけ。メンテナが新しい API をブラウザで開き、取り込みが使う 8 つのエンドポイント(`courses`、`problems?size=10000`、`challenges` の 6 種別)のパスと応答の形が fixture と同じことを確かめた | -| fixture は取り直さない | 応答の形が同じため。取り直すと統計値(`solvedUser` など)や新しい回の追加で差分が出て、fixture に依存するテストの期待値まで変わる | -| テストは nock のまま、クライアントを本番の定数から作る | CI で実 API を叩かないために nock でモックするのはメンテナの意図どおりで、そのまま残す。問題は、テストが自前の旧ホストからクライアントを作っていて、定数を間違えても通ることだった | -| `nock.disableNetConnect()` を足す | モックを書き忘れたリクエストが、黙って実 API に飛ぶのを防ぐ | -| 許可リストの旧ホストは残さず、新しいホストに置き換える | 旧ホストは 410 を返すだけで使い道が無い。許可先はデータを持ち出せる経路でもあるので、数を増やさない | - -### 検証で分かったこと - -- **応答の違い**: 値の違いは最上位の `largeCl.id` が小文字になったこと(`"JAG"` → `"jag"` など)だけ。コードはここを読まない。fixture に無い回(ICPC 国内予選 2026、JAG 模擬国内 2026、PCK 予選 2026 など)は、fixture の記録(2026-05)の後に AOJ に追加されたもの -- **ベース URL の落とし穴**: `new URL(endpoint, base)` は、ベースの末尾に `/` が無いか、エンドポイントの先頭に `/` があると `/api` を落とす。旧ホストはパスが無かったので表に出ていなかった -- **テストの検出力**: `AOJ_API_BASE_URL` の 4 つの変異(末尾の `/` を外す、旧ホストに戻す、`/api/v1/` にする、`/api` を外す)は、どれも 26 件すべてのテストで検出された -- **問題ページへのリンク**: `https://onlinejudge.u-aizu.ac.jp/problems/{id}` は今のまま開ける(メンテナが確認) -- **合宿系の API**: JAG の filter には summer / winter / spring もあるが、既存の contest_id にそろえるアドホックな対応が多いので、応答の形は意図的に確かめていない([plan-pr2-1.md](./plan-pr2-1.md#csv)) -- **開発コンテナからの 500**: ベース URL を変えた後も、ローカルの管理画面の取り込みが 500 になった。開発サーバーはコンテナの中で動き、外向き通信は許可リスト方式で、新しいホスト(163.143.87.177)が入っていなかった(旧ホストは 163.143.87.219)。サーバーの `fetch` が接続で拒否され、管理画面のアクションが `fail(500)` を返していた。ブラウザはホストの Mac から直接開くので見えていた -- **許可リストの反映**: `init-firewall.sh` は Dockerfile の `COPY` でイメージに入り、起動時に 1 回だけ DNS で IP を引く。編集を反映するには、コンテナの再起動ではなく再ビルドが要る - -### 残っている確認 -- ~~コンテナを再ビルドした後、ローカルの管理画面で AOJ から取得できること~~ — 2026-10-24 にメンテナが確認した -- ~~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)`) +- **PR0**: 出典欄の AOJ 分岐を `${contestName} - ${taskTableIndex}(AOJ)` にし、AOJ のコンテスト名ラベルから全角括弧を外した +- **PR1**: ICPC 国内予選 / アジア地区 / JAG 模擬国内(79 コンテスト / 632 問)の `task_table_index` を問題記号にした。移行 SQL は `prisma/migrations/20261024033928_update_aoj_task_table_index/`。管理画面の取り込みでも、この範囲は基本ルールで記号を付ける。表示側の変換(`buildAojLetterMap` / `AOJ_LABEL_OVERRIDES` / `formatAojTitle` / `getTaskLabels`)は削除した +- **PR1.5**: `AOJ_API_BASE_URL` を `https://judgeapi.u-aizu.ac.jp/` から `https://onlinejudge.u-aizu.ac.jp/api/` に変えた。旧ホストは 410 を返し、管理画面からの AOJ の取り込み(Courses / PCK / JAG / ICPC)がすべて失敗していた + - `clients.test.ts` のクライアントを本番の定数から作り、nock で受ける URL(`/api/…`)を期待値として直書きした。`nock.disableNetConnect()` も足した + - 開発コンテナの外向き通信の許可リスト(`.devcontainer/init-firewall.sh`)の `judgeapi.u-aizu.ac.jp` を `onlinejudge.u-aizu.ac.jp` に置き換えた +- **PR2**: 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(模擬国内 / 模擬地区)で記号を付けるようにした +- **PR2.1**: ContestType `ATCODER_JAG` を新設し、AtCoder 上の JAG 4 コンテスト(`jag2018summer-day2` / `jag2017summer-day1` / `jag2015summer-day3` / `jag2013summer-warmingup`)を完全一致で分類するようにした。マイグレーションは `prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/` + - seed に、この 4 コンテスト(42 問)と、2025 / 2026 の JAG 夏合宿(`JAGSummer2025-day1`〜`JAGSummer2026-day3`、AOJ 3648–3719、6 コンテスト / 72 問、記号 A–L・title は素)を足した。値はメンテナから受け取った(AtCoder 側は AtCoder Problems の値) + +## 進め方 + +| 決定 | PR | 理由 | +| ---------------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| PR0 → PR1 → 合宿系の順にする | 全体 | PR0 は DB に依存しない。PR1 は出典欄の成果がすぐ見え、欠落が少ないので対応表の形式を楽なケースで決められる。逆順はレビュー負荷が低いが、出典欄の反映が遅れ、対応表の形式を最も判断の重い合宿系で作ることになる | + +## 移行 SQL + +| 決定 | PR | 理由 | +| ----------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 対応表を焼き込まず、基本ルールを SQL で表す | PR1 | 本番のタスクは管理画面の取り込みで入っており、seed と一致する保証が無い。`VALUES` で書くと本番にしか無い行が残り、`UPDATE` は該当行が無くてもエラーにならないので、取りこぼしが静かに起きる | +| 基本ルールで振り、8 コンテストはコンテストの全行を上書きする(45 行) | PR2 | 8 コンテストは先頭や途中の問題が AOJ に無く、基本ルールでは記号がずれる。正しい記号は、メンテナのスプレッドシートの位置(欠けた問題を飛ばした位置)で決まる。全行を上書きするのは、1 つのコンテストに 2 つの規則を混ぜないため | +| 範囲は `contest_type` ではなく `contest_id` の接頭辞で絞る | PR1 / PR2 | 合宿系も模擬国内と同じ `AOJ_JAG` に分類される。PR1 で `contest_type` だけで絞ると合宿系 467 件まで更新し、title のプレフィックスが表に出る。PR2 は `^JAG(Summer\|Winter\|Spring\|Regional)` で絞り、PR1 で記号にした模擬国内を外す | +| 各ステップの更新件数を DO ブロックで検査し、想定外なら RAISE する | PR2 | UPDATE は対象が 0 件でもエラーにならない。想定値は、空の DB(`prisma migrate reset`)の 0、模擬地区の無い開発 DB の 467 / 32 / 467、staging・本番の 625 / 45 / 467 | +| 件数は固定値のまま。模擬地区が一部だけの DB では RAISE し、エラー文で「全部取り込むか、全部消す」と案内する | PR2 | 本番で範囲の誤りを止める力を保つため。模擬地区だけ実際の件数に合わせる案は、範囲の誤りを見逃しやすくなるので採らなかった | +| 順位はコンテストの全行で付け、更新は問題 ID のままの行だけにする | PR2 | 後から記号付きで入る行(PR2.1)が先にあっても触らないため。順位を付ける側でも絞ると、記号の行と問題 ID の行が混ざったコンテストで A から振り直してしまう(Codex の指摘) | +| title から外すのは「自分の `task_id` + `. `」で始まるものだけ | PR2 | `^[0-9]+\. ` で外すと、番号で始まる本来の title まで削りうる | +| `AOJ-KUPC2011` の 2279 は title だけを素にし、index(`I`)は触らない | PR1 | 本番でも index は既に正しい。AOJ に 1 問しか無いので、大学系に範囲を広げるときは基本ルール(`A` になる)の対象から外す | + +分かったこと: + +- **SQL**(PR1): `row_number()` は `bigint` を返し、`chr(bigint)` は存在しないので `::int` が要る。`EXPLAIN` は `task` の Seq Scan 2 回 + Hash Join(`contest_type` に索引が無いため想定どおり) +- **トランザクション**(PR1): マイグレーションはファイル単位で原子的に適用される(3 文目が失敗するマイグレーションで、1–2 文目も残らないことをローカルで確認) +- **ローカルでの移行**(PR2): 管理画面で模擬地区を取り込み、本番と同じ状態にしてから確かめた。`SELECT` のプレビューは 625 行すべてで照合の結果と一致し、適用後もプレビューと 625 / 625 で一致した。範囲外の AOJ(PCK、大学系、PR1 の範囲)は変わらない。`EXPLAIN` は CTE 側の Seq Scan 1 回と、`task_task_id_key` の Index Scan を使う Nested Loop +- **Codex の指摘の再現**(PR2): 記号の行と問題 ID の行が混ざったコンテストを読み取り専用で再現すると、直す前の SQL は A から振り直し、直した後は B, C, D と正しく振った。ローカルは移行前のスナップショットに戻し、直した移行を適用し直した(625 / 625)。「`task_id` だけで結合している」という指摘は、`task_id` が schema で `@unique` なので該当しない +- **staging**(PR2): 当初は模擬地区が 2 コンテスト / 16 問しか無く、件数チェックで RAISE するところだった。PR1.5 の後に staging で取り込み、PR を開く前に本番と同じ状態(模擬地区 16 / 158、合宿系 50 / 467、index はすべて問題 ID、title の問題 ID は合宿系だけ)であることを確かめた。#4220 で反映した(2026-10-25)。移行の後に Vercel のデプロイが走るので、件数チェックも通っている + +## 照合(記号の対応表) + +- **PR1**: メンテナのスプレッドシートを 2026-10-21 のスクリーンショットから書き写し、632 問すべてが基本ルールの記号の位置で一致した(補正なし)。ICPC 国内予選 2002 の E(Enclosing Circles)は AOJ に 1123 として存在するが、コンテスト一覧に無いので取り込まれていない(末尾なので記号はずれない。PR3 の対象) +- **PR2**: スプレッドシートの書き写し(66 コンテスト)と 625 問の title が、記号の位置ですべて一致した。補正は 8 コンテスト / 45 行(事前の目視どおり) +- **PR2.1**: 114 問すべてが、スプレッドシートの問題名と記号の位置ごとに一致した(目視)。AOJ の 6 コンテストは問題 ID が連番で欠落が無く、基本ルールと一致する +- **照合スクリプト**: PR1 は `git show 171bf3c9:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_problem_letters.ts`、PR2 は `git show c605d3bd:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_jag_problem_letters.ts`。`prisma/tasks.ts` を import し、件数が既知の値と違えば止まる。title は大文字小文字・空白・記号を無視して比べ、セルで切れた title は前方一致にする。スプレッドシート自体の誤りは検出できない +- **スクリプトの検出力**: わざと誤りを入れたコピーを、PR1 / PR2 とも 5 種類すべて検出した。PR1 は順番の入れ替え、途中の title の誤り、途中の欠番、問題数の不足、切れた title の前半の誤り。PR2 は欠けた記号を外す、順番の入れ替え、途中の title の誤り、欠けた数の不足、切れた title の前半の誤り + +## 取り込み(管理画面・AOJ API) + +| 決定 | PR | 理由 | +| ---------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 取り込みでは、対応表ではなく基本ルールで記号を付ける(M5) | PR1 | 対応表を AOJ client に置く案は、毎年の新しいコンテストで追記が要り、忘れると数値の index が黙って入る。`prisma/tasks.ts` と同じ情報を 2 か所に持つことにもなる | +| 取り込みでは補正しない(M14) | PR2 | 8 コンテストは本番・staging・seed のすべてにあり、取り込みは既存の問題を skip する。Codex も「取り込みで補正すべき」と指摘したが、同じ理由で採らなかった | +| 合宿系は管理画面の取り込みに足さない | PR2.1 | 合宿系も AOJ API の JAG の filter(summer / winter / spring)にあるが、既存の contest_id(`JAGSummer2012-day3A` など)にそろえるアドホックな対応が多く、年 1 回の追加には重い。応答の形は意図的に確かめていない(メンテナの判断) | +| AOJ API はベース URL だけを変える | PR1.5 | AOJ Developers の告知(2026-09-07 に Main API が移転)で変わったのはホストだけ。メンテナが新しい API をブラウザで開き、取り込みが使う 8 つのエンドポイント(`courses`、`problems?size=10000`、`challenges` の 6 種別)のパスと応答の形が fixture と同じことを確かめた | + +分かったこと: + +- **2 日開催の JAG 模擬国内**(PR1): AOJ API には `JAGPrelim20162`(2738–2751)のように 2 日を 1 コンテストにしたものがある。取り込みは `contest_id` 単位で振るので A–N になる。seed と本番は `JAGPrelim2016A` / `B` に分かれており、取り込みは既存の問題を skip するので影響しない +- **応答の違い**(PR1.5): 値の違いは最上位の `largeCl.id` が小文字になったこと(`"JAG"` → `"jag"` など)だけ。コードはここを読まない。fixture に無い回(ICPC 国内予選 2026、JAG 模擬国内 2026、PCK 予選 2026 など)は、fixture の記録(2026-05)の後に AOJ に追加されたもの +- **ベース URL の落とし穴**(PR1.5): `new URL(endpoint, base)` は、ベースの末尾に `/` が無いか、エンドポイントの先頭に `/` があると `/api` を落とす。旧ホストはパスが無かったので表に出ていなかった +- **問題ページへのリンク**(PR1.5): `https://onlinejudge.u-aizu.ac.jp/problems/{id}` は今のまま開ける(メンテナが確認) +- **staging(Vercel)での取り込み**(PR1.5): 2026-10-24 にメンテナが JAG REGIONAL を取り込み、16 / 158 になった(PR2 の前提)。こちらの取得ツールが新しいホストに `ECONNREFUSED` で拒まれたことから送信元の制限を心配していたが、Vercel からは問題なく取得できた + +## 分類と表示 + +| 決定 | PR | 理由 | +| ----------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AOJ ラベルから全角括弧を外す(M11) | PR0 | 出典欄の目標書式ではコンテスト名を括弧で囲まない。ラベルは検索・投票・タグ・取り込み画面・提出状況の更新モーダルでも使われており、その表示も変わるが許容した | +| 表示側の変換と `getTaskLabels` を削除する(M7 / M10) | PR1 | DB が記号を持てば変換は恒等になる。インターフェイスの `getTaskLabels` だけを残す案は、上書きする Provider が 0 になり使われないコードが残るので採らなかった | +| ヘッダーの並びは `localeCompare`(M4) | PR1 | 記号に `Number()` を当てると NaN になり、ソートが効かなくなる | +| `buildTitleWithTaskIndex` は「外してから付ける」 | PR1 | AtCoder 側の title は `A. …` を含むので、付けるだけだと二重になる。title にプレフィックスの無い AtCoder の問題は `abs` の practice_1 だけで(本番も同じ)、ABS は index を表示しない | +| `removeTaskIndexFromTitle` は先頭のプレフィックスだけを消す(レビュー) | PR1 | 記号は title の本文にも現れうる。以前は index が空のとき `Dr. Podboq` → `DrPodboq` のように本文の `. ` まで消していた | +| `ATCODER_JAG` を新設し、4 つを contest_id の完全一致で分類する(M17) | PR2.1 | `jag` の前方一致だと、AtCoder Problems の `JAG2013Spring` / `JAG2014Spring`(AOJ の `JAGSpring2013` / `2014` と同じコンテスト)まで入り、問題が二重になる。`OTHERS` に入れないのはメンテナの判断。`AOJ_JAG` だと出典欄の `(AOJ)` や `getTaskUrl` が AOJ を前提に動く | +| ラベル(出典欄)に(有志)を付けない | PR2.1 | PR2.5 のテーブル名でだけ、2018 Day2 / 2017 Day1 に付ける(M16) | + +分かったこと: + +- **表示の回帰**(PR1): 旧コードで適用前のデータを、新コードで適用後のデータを描画し、79 コンテストすべてで列順とセルの文字列が一致した。スクリーンショットでの目視は未実施 +- **DB の `contest_type`**(PR2.1): 書き込むだけで、表示などで読む箇所は無い(分類は毎回 `contest_id` から引く)。enum の値は `ADD VALUE` で DB 上の末尾に足され、schema.prisma の並びと違うが、並べ替えにも使っていない + +## seed とデータの投入 + +| 決定 | PR | 理由 | +| ------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 前後に空白のある title(本番 46 件、3355 の末尾のタブを含む)は直さない | PR2 | 表示では空白が潰れ、完全一致で比べるコードも無い。取り込みが trim するので再発もしない([plan.md](./plan.md#スコープ外)) | +| staging / 本番の 2025 / 2026 夏合宿は、PR2 の移行の後に Supabase の CSV で記号付きで入れる | PR2.1 | 移行の前に問題 ID のまま入れると、PR2 の件数の想定値が変わる。記号付きなら移行と独立する | +| CSV の `id` は既存の合宿系と同じ `sha256(contest_id + title)` | PR2.1 | 取り込みの `sha256(contest_id + task_id)` とは違うが、`Task.id` を再計算して引く箇所が無い([根拠](./investigation.md#id-の採番規則が経路ごとに違う)) | +| seed の AOJ 72 行は古い順・問題 ID の昇順に置く | PR2.1 | add-tasks の規則(新しい順・降順)は AtCoder 側の慣習で、既存の AOJ のブロックはすべて昇順。Codex の指摘は採らなかった | + +分かったこと: + +- **seed の模擬地区**(PR2): 本番から取り出した 158 問は、AOJ の fixture(2026-05 記録)と問題 ID・コンテスト・名前がすべて一致した +- **ローカルの取り込み**(PR2.1): 2026-10-25 にメンテナが AtCoder の 4 コンテストを取り込み、42 行が task_id・contest_id・記号・title まで seed と一致した + +### PR2.1 をロールバックするとき + +取り込みの後に PR2.1 より前の版へ戻すと、旧版の Prisma Client は `ATCODER_JAG` を読めない。取り込みは既存の task_id を skip するので、戻した値はロールフォワードしても残る。 + +1. ロールバックの前に、該当行を `OTHERS` に変え、返った `expected_count` を環境名とともに控える(全問を取り込んでいれば 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. ロールフォワードでは、PR2.1 以降をデプロイしてから、`expected_count` を控えた件数に置き換えて実行する。件数が違えば `RAISE` で UPDATE ごと取り消されるので、期待値を書き換えて通さず、記録と対象行を調べる。最後の SELECT が `ATCODER_JAG` の 1 行(件数は控えた値)だけを返せば復旧完了。控えた件数が 0 なら SELECT も 0 行になる。`OTHERS` など別の分類が残れば、復旧は終わっていない + +```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"; +``` + +## テスト + +| 決定 | PR | 理由 | +| ------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| seed の検査は、補正した 8 コンテストを literal で、title を「問題名(`name`)と同じ」で見る | PR2 | 補正の正しさは基本ルールの関数では検査できない。title は「問題 ID が前に付いていない」だけだと、空や別の問題の title にすり替わっても通る(Codex の指摘) | +| AOJ の fixture は取り直さない | PR1.5 | 応答の形が同じため。取り直すと統計値(`solvedUser` など)や新しい回の追加で差分が出て、fixture に依存するテストの期待値まで変わる | +| AOJ のテストは nock のまま、クライアントを本番の定数から作る | PR1.5 | CI で実 API を叩かないために nock でモックするのはメンテナの意図どおりで、そのまま残す。問題は、テストが自前の旧ホストからクライアントを作っていて、定数を間違えても通ることだった | +| `nock.disableNetConnect()` を足す | PR1.5 | モックを書き忘れたリクエストが、黙って実 API に飛ぶのを防ぐ | + +分かったこと(`verify-test-strength` による変異テスト): + +- **PR1**: 15 個の変異をすべて検出した。seed 整合テストの `ICPC Prelim 2024 F is problem 1677` は単独で検出する変異が無いが、唯一の literal な期待値(対応表とデータが一緒にずれたときの歯止め)なので残した +- **PR1.5**: `AOJ_API_BASE_URL` の 4 つの変異(末尾の `/` を外す、旧ホストに戻す、`/api/v1/` にする、`/api` を外す)は、どれも 26 件すべてのテストで検出された +- **PR2**: 6 つの変異をすべて検出した(JAG の記号を PRELIM だけ / ICPC だけにする、補正した記号を基本ルールに戻す、基本ルールのコンテストで A・B を入れ替える、title に問題 ID を付け直す、模擬地区の記号を問題 ID に戻す)。title のテストは、空の title・別の問題の title・問題 ID の付け直しも検出する +- **PR2.1**: 9 つの変異をすべて検出した(seed の記号の入れ替え・行の欠落・title の不一致、map の欠落、優先度の入れ替え、`jag` / `jag2` の前方一致、4 つのキーでの前方一致、ラベルの定数化)。4 つのキーでの前方一致は当初見逃しており、Codex の指摘でテストを足した + +## 開発環境(dev container) + +| 決定 | PR | 理由 | +| ------------------------------------------------------ | ----- | --------------------------------------------------------------------------------------------------- | +| 許可リストの旧ホストは残さず、新しいホストに置き換える | PR1.5 | 旧ホストは 410 を返すだけで使い道が無い。許可先はデータを持ち出せる経路でもあるので、数を増やさない | + +分かったこと(ローカルの管理画面の取り込みが 500 になった原因。ブラウザにはどちらも「Failed to load resource: 500」とだけ出る): + +- **許可リスト**(PR1.5): 開発サーバーはコンテナの中で動き、外向き通信は許可リスト方式で、新しいホスト(163.143.87.177)が入っていなかった(旧ホストは 163.143.87.219)。サーバーの `fetch` が接続で拒否され、管理画面のアクションが `fail(500)` を返していた。ブラウザはホストの Mac から直接開くので見えていた。`init-firewall.sh` は Dockerfile の `COPY` でイメージに入り、起動時に 1 回だけ DNS で IP を引くので、編集を反映するにはコンテナの再起動ではなく再ビルドが要る。再ビルドの後、2026-10-24 にメンテナがローカルで AOJ から取得できることを確かめた +- **古い Prisma Client**(PR2.1): enum を足す前から動いている開発サーバーは、古い Prisma Client を持ったままで、`ATCODER_JAG` の登録が 500 になる。Vite はソースの変更を反映するが `node_modules` は読み込み直さないので、一覧には新しいコンテストが出る。開発サーバーの再起動で直る 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 6e4e18bdf..d47f9d332 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 問)を目視で突き合わせた。目視の結果で、PR2 の照合スクリプト(書き写したスプレッドシートとの機械的な照合)でも同じ結果になった([done.md](./done.md#pr2--jag-合宿系と-jag-模擬地区を問題記号にする4030))。 +メンテナのスプレッドシート(スクリーンショット 16 枚、行 2〜1025。上が新しい)と、本番の問題一覧の JAG 模擬地区(スクリーンショット 10 枚、158 問)を目視で突き合わせた。目視の結果で、PR2 の照合スクリプト(書き写したスプレッドシートとの機械的な照合)でも同じ結果になった([done.md](./done.md#照合記号の対応表))。 スプレッドシートで折りたたまれた行(40–50 ほか 19 か所)は JAG 模擬国内だけで、合宿系・模擬地区は含まない(メンテナに確認済み)。 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 deleted file mode 100644 index d19b66221..000000000 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-1.md +++ /dev/null @@ -1,157 +0,0 @@ -# PR2.1 — DB に無い 10 コンテストを入れる - -前提・共通の決定・データ移行の手順は [plan.md](./plan.md)。スプレッドシートとの対応は [investigation.md](./investigation.md#db-に無いコンテスト)。 - -**状態**: 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 と前後してよい。 - -## 範囲 - -メンテナのスプレッドシートにあって、seed にも本番にも無いもの。 - -| コンテスト | contest_id | 問題(task_id) | 入手元 | 本番への入れ方 | -| ------------------------------------- | ---------------------------- | ------------------------------------------ | ------- | ------------------------------------- | -| JAG 夏合宿 2026 Day1〜3 | `JAGSummer2026-day1`〜`day3` | 3684〜3719(各 12 問、A〜L) | AOJ | Supabase の CSV(既存の合宿系と同じ) | -| JAG 夏合宿 2025 Day1〜3 | `JAGSummer2025-day1`〜`day3` | 3648〜3683(各 12 問、A〜L) | AOJ | 同上 | -| JAG 夏合宿 2018 Day2(有志) | `jag2018summer-day2` | `jag2018summer_day2_a`〜`k`(11 問) | AtCoder | 管理画面の AtCoder 取り込み | -| JAG 夏合宿 2017 Day1(有志) | `jag2017summer-day1` | `jag2017summer_day1_a`〜`k`(11 問) | AtCoder | 同上 | -| JAG 夏合宿 2015 Day3(過去問) | `jag2015summer-day3` | `icpc2015summer_day3_a`〜`j`(10 問) | AtCoder | 同上 | -| JAG 夏合宿 2013 Day2 WarmUp(過去問) | `jag2013summer-warmingup` | `icpc2013summer_warmingUp_a`〜`j`(10 問) | AtCoder | 同上 | - -計 114 問(AOJ 72、AtCoder 42)。いずれもスプレッドシートの問題名と記号の位置ごとに一致した(目視)。 - -- AOJ の 6 コンテストは問題 ID が連番で、欠落も無い。記号は基本ルールと一致する。メンテナから受け取った値は `prisma/tasks.ts` と同じ形(`id` / `contest_id` / `problem_index` / `name` / `title`)で、title は素 -- AtCoder の 4 コンテストは AtCoder Problems の値で、title は `A. …` の形。2015 / 2013 の task_id は `icpc…` で始まるが、分類と URL はどちらも contest_id で決まるので影響しない - -関係する決定: M17([plan.md](./plan.md#決定事項))。 - -## タスク - -| # | 内容 | 補足 | -| ----- | --------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| 2.1-0 | `ATCODER_JAG` を新設する | [下記](#atcoder_jag)。`add-contest-table-provider` の Layer 1–3 | -| 2.1-1 | in AtCoder の 4 コンテストを seed に追加する | 上の値。本番は管理画面の AtCoder 取り込みで入る(分類されれば skip されない) | -| 2.1-2 | 2025 / 2026 夏合宿の 6 コンテストを seed に追加する | メンテナから受け取った値をそのまま使う | -| 2.1-3 | staging / production に Supabase の CSV で入れる | **その環境に PR2 の移行を適用した後**に入れる。[下記](#csv) | -| 2.1-4 | テスト | 分類・優先度・ラベル(Layer 3)。seed の記号の検査(PR2 で広げたもの)に 2025 / 2026 を足す | - -**クロスレビューが必須**(enum の追加は DB schema の変更)。 - -## ATCODER_JAG - -| 場所 | 変更 | -| --------------------------------------------- | ---------------------------------------------------------------- | -| `prisma/schema.prisma` の `ContestType` enum | `UNIVERSITY` の直前に追加し、マイグレーションを作る | -| `src/lib/contests/types/contest.ts` | `ContestType` に同じ位置で追加 | -| `contestTypePriorities`(`priority.ts`) | 18 にして、`UNIVERSITY` 以降を +1 ずつずらす。JSDoc の範囲も直す | -| `CLASSIFICATION_RULES`(`classification.ts`) | `UNIVERSITY` の規則の直前に、下の 4 つと完全一致する規則を置く | -| `prefixes.ts` | `ATCODER_JAG`(contest_id → ラベル、下表) | -| `LABEL_GENERATORS`(`labels/index.ts`) | `ATCODER_JAG` の map からラベルを引く | - -| contest_id | ラベル(出典欄) | -| ------------------------- | ----------------------------- | -| `jag2018summer-day2` | `JAG 夏合宿 2018 Day2` | -| `jag2017summer-day1` | `JAG 夏合宿 2017 Day1` | -| `jag2015summer-day3` | `JAG 夏合宿 2015 Day3` | -| `jag2013summer-warmingup` | `JAG 夏合宿 2013 Day2 WarmUp` | - -ラベルに(有志)は付けない。PR2.5 のテーブル名でだけ、2018 Day2 と 2017 Day1 に `(有志)` を付ける(M16)。 - -- 優先度をずらしても、`src/test/lib/utils/task.test.ts` の優先度の差の期待値は直さずに済んだ(0〜11 の項目しか含まない) -- 4 つの contest_id は小文字の `jag…` で始まるので、`regexForJag`(大文字の `JAG(Prelim|…)`)にも `AojGenerator` の `startsWith('ICPC')` にも当たらない。`getTaskUrl` は AtCoder の URL を作る - -却下した方針: - -| 案 | 却下理由 | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `jag` の前方一致で分類する | AtCoder Problems の `JAG2013Spring` / `JAG2014Spring` は AOJ の `JAGSpring2013` / `2014` と同じコンテストで、問題が二重に入る | -| `OTHERS` に入れる | メンテナの判断で専用の ContestType にした | -| `AOJ_JAG` に入れる | 出典欄の AOJ 分岐(`…(AOJ)`)や `getTaskUrl` が AOJ を前提に動く | - -## CSV - -既存の合宿系と同じく、メンテナが別プロジェクトのスクリプトで作る。 - -- `id` は `sha256(contest_id + title)`(16 進。既存の合宿系と同じ規則)。管理画面の取り込みは `sha256(contest_id + task_id)` で規則が違うが、`Task.id` を再計算して引く箇所は無いので問題ない([根拠](./investigation.md#id-の採番規則が経路ごとに違う)) -- `contest_type` は `AOJ_JAG`、`task_table_index` は記号、`title` は素(受け取った値のまま) -- `updated_at` は DB に既定値が無い(`@updatedAt` は Prisma 側の実装)ので、CSV に含まれていることを確かめる。`created_at` / `grade` / `atcoder_problems_difficulty` には既定値がある -- 投入は `invalidateTaskCaches()` を経ないので、反映はキャッシュの TTL(1 時間)の後になる - -却下した方針: - -| 案 | 却下理由 | -| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 移行の前に、既存の合宿系と同じ形(問題 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 される -- ローカルの取り込みで入った 42 行は、task_id・contest_id・記号・title のすべてが seed と一致した。開発サーバーを enum の追加より前から動かしていると、古い Prisma Client が `ATCODER_JAG` を受け付けず、登録が 500 になる(再起動で直る) - -### 本番で気をつけること - -- AtCoder の 4 コンテストの取り込みは、本番のデプロイが終わってから行う -- 取り込みの後に 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) - -| # | 指摘 | 対応 | -| --- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -| 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-pr2-5.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md index e8434600e..8f39e41c2 100644 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md +++ b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md @@ -18,7 +18,7 @@ | `JAG 夏合宿 2015 Day3` | なし(in AtCoder・過去問) | - 出典欄は変えない。この配列は #4018 でコンテスト情報を DB に移すときの移行元になる -- 配列には PR2.1 の 10 件も入れる。データが無い間は表示されないので、PR2.1 より先に出してよい(contest_id は確定済み。[plan-pr2-1.md](./plan-pr2-1.md#範囲)) +- 配列には PR2.1 の 10 件も入れる。データが無い間は表示されないので、PR2.1 より先に出してよい(contest_id は確定済み。[done.md](./done.md#何をしたか)) - Provider の `contestType` は全件 `AOJ_JAG` にする。provider key の名前空間として使うだけで、`getTaskTableHeaderName` での違いは ABC の H/Ex だけなので、in AtCoder の 4 件も同じでよい。`ATCODER_JAG` に依存しないので、PR2.1 と順序を問わない 関係する決定: M15 / M16([plan.md](./plan.md#決定事項))。 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 fd058e5ee..07450166c 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,15 +1,14 @@ # AOJ ICPC / JAG の `problem_index` を A/B/C 形式へ変更する -**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)は staging に反映、本番は未反映。PR2.1(#4221)は実装済み(PR 前) +**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)・PR2.1(#4222)は staging に反映、本番は未反映 このファイルは全体像と、PR をまたぐ決定・手順だけを書く。各 PR の作業はそれぞれのファイルにある。 | ファイル | 内容 | | -------------------------------------- | -------------------------------------------------- | -| [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 の決定の要約と残タスク | +| [done.md](./done.md) | PR0〜PR2.1 の決定の要約と残っている作業 | | [investigation.md](./investigation.md) | 根拠データ。PR2 の照合結果、表示順、欠落の一覧など | ## ゴール @@ -37,15 +36,17 @@ AOJ の API は 2026-09-07 に別のホストへ移り、旧ホストは 410 を PR2(#4220)で、合宿系と模擬地区も記号になり、合宿系の title は素になった。管理画面の取り込みも、JAG は全 round で記号を付ける(staging に反映。本番は未反映)。 +PR2.1(#4222)で、AtCoder 上の JAG 4 コンテスト用に `ATCODER_JAG` を新設し、seed に 4 コンテストと 2025 / 2026 の夏合宿を足した(staging に反映。staging / 本番へのデータの投入は未)。 + ## PR の分割と順序 -| | 範囲 | コンテスト | 問題数 | 内容 | -| ------------------------------------------------------------------- | ------------------------------------------------------- | ---------- | ------------------------- | --------------------------------------------------------- | -| [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.5](./done.md) | AOJ の API クライアント | — | — | ベース URL を新しいホストへ変える(#4028)。完了(#4219) | +| [PR2](./done.md) | JAG 夏合宿 / 冬合宿 / 春合宿 + JAG 模擬地区 | 66 | 625 | 既存データを記号にする。staging に反映(#4220) | +| [PR2.1](./done.md) | 2025 / 2026 の夏合宿(AOJ)+ in AtCoder の 4 コンテスト | 10 | 114(スプレッドシート上) | 新しいデータを入れる(`ATCODER_JAG` の新設を含む)。staging に反映(#4222) | +| [PR2.5](./plan-pr2-5.md) | PR2 / PR2.1 の 76 コンテスト | 76 | — | グループ「JAG 模擬地区」に、コンテスト別のテーブルを作る | +| [PR3](./plan-pr3.md) | 欠落問題の補完 | — | — | AOJ のコンテスト一覧に無い問題をどう埋めるか | PR1 と PR2 で、記号にするのは 1,257 問 / 145 コンテストになる。PR2 で完結し、PR3 に進まなくても機能として成立する。 @@ -156,12 +157,12 @@ PR2(記号の移行)と PR2.1(enum の追加)、PR3 に当てはまる | **バックアップ** | Supabase Pro のため PITR が利用可。復元トリックが効くのは一度目だけなので、**再修正時は PITR が唯一の手段** | | **`updated_at`** | `@updatedAt` は Prisma Client 側の実装で DB トリガーではない。生 SQL では更新されない。**意図的に含めない** | | **キャッシュ** | TTL 1 時間(`src/lib/clients/cache.ts`)。生 SQL と CSV の投入は `invalidateTaskCaches()` を経由しない。「更新したのに表示が変わらない」ときはこれを疑う | -| **トランザクション境界** | マイグレーションファイル単位で原子的に適用される([done.md](./done.md#検証で分かったこと)) | +| **トランザクション境界** | マイグレーションファイル単位で原子的に適用される([done.md](./done.md#移行-sql)) | | **デプロイ直前の数分** | マイグレーションの適用から Vercel のデプロイ完了までの間は、旧コードが新しいデータを描画する。数分で解消するので**許容する** | ### スプレッドシートとの照合 -対応表は次の手順で作る(M6)。PR1 のスクリプトを直して使う(仕組みは [done.md](./done.md#検証で分かったこと))。 +対応表は次の手順で作る(M6)。PR1 のスクリプトを直して使う(仕組みは [done.md](./done.md#照合記号の対応表))。 1. 基本ルールで対象範囲の記号を生成する 2. コンテスト単位で、メンテナのスプレッドシートと照合する ← 人手が要るのはここだけ(書き写し) From 33ef153fda4440a0718505ce6ec58ee78dc1943a Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 05:51:09 +0000 Subject: [PATCH 2/5] docs: record the PR2.1 data load on staging Co-Authored-By: Claude Sonnet 5.5 --- .../task-title-index-change-impact/done.md | 18 +++++++++--------- .../task-title-index-change-impact/plan.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) 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 e382d8ef2..83908a21d 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 @@ -2,13 +2,13 @@ コードと git の履歴から読み取れない判断と事実だけを、項目ごとにまとめる。変更したファイルや関数は各 PR の差分を見る。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 -| PR | 内容 | 状態 | -| -------------- | --------------------------------------------------------------------- | -------------------------------------- | -| PR0(#4212) | 出典欄の AOJ の書式 | 完了 | -| PR1(#4217) | ICPC 国内予選 / アジア地区 / JAG 模擬国内(79 / 632)を問題記号にする | 完了 | -| PR1.5(#4219) | AOJ API の移転に追従する(#4028) | 完了 | -| PR2(#4220) | JAG 合宿系と JAG 模擬地区(66 / 625)を問題記号にする(#4030) | staging に反映、本番は未反映 | -| PR2.1(#4222) | DB に無い 10 コンテストを入れる(#4221) | staging に反映、データの投入と本番は未 | +| PR | 内容 | 状態 | +| -------------- | --------------------------------------------------------------------- | ------------------------------------------------ | +| PR0(#4212) | 出典欄の AOJ の書式 | 完了 | +| PR1(#4217) | ICPC 国内予選 / アジア地区 / JAG 模擬国内(79 / 632)を問題記号にする | 完了 | +| PR1.5(#4219) | AOJ API の移転に追従する(#4028) | 完了 | +| PR2(#4220) | JAG 合宿系と JAG 模擬地区(66 / 625)を問題記号にする(#4030) | staging に反映、本番は未反映 | +| PR2.1(#4222) | DB に無い 10 コンテストを入れる(#4221) | staging に反映(データも投入済み)、本番は未反映 | ## 残っている作業 @@ -16,8 +16,7 @@ - PR2: 本番(`main` への昇格後)で、移行が通ること - PR2: 問題一覧の出典欄の表示(例: `JAG 夏合宿 2024 Day3 - H(AOJ)`) -- PR2.1: staging で、管理画面から AtCoder の 4 コンテストを取り込み、2025 / 2026 夏合宿の CSV を投入する(PR2 の移行は適用済み) -- PR2.1: 本番(`main` への昇格後)で、デプロイが終わってから 4 コンテストを取り込み、PR2 の移行が通った後に CSV を投入する +- PR2.1: 本番(`main` への昇格後)で、デプロイが終わってから 4 コンテストを取り込み、PR2 の移行が通った後に CSV を投入する(staging と同じ手順) - PR2.1 の CSV には `updated_at` を含める(`@updatedAt` は Prisma 側の実装で、DB に既定値が無い。`created_at` / `grade` / `atcoder_problems_difficulty` には既定値がある)。投入は `invalidateTaskCaches()` を経ないので、反映はキャッシュの TTL(1 時間)の後になる - PR1: コンテストテーブルのスクリーンショットでの目視確認 @@ -124,6 +123,7 @@ PR2 以降の計画には含めていない。 - **seed の模擬地区**(PR2): 本番から取り出した 158 問は、AOJ の fixture(2026-05 記録)と問題 ID・コンテスト・名前がすべて一致した - **ローカルの取り込み**(PR2.1): 2026-10-25 にメンテナが AtCoder の 4 コンテストを取り込み、42 行が task_id・contest_id・記号・title まで seed と一致した +- **staging への投入**(PR2.1): 2026-10-25 にメンテナが、管理画面から AtCoder の 4 コンテストを取り込み、2025 / 2026 夏合宿の CSV を投入した。10 コンテストの行は `ATCODER_JAG` 42 行・`AOJ_JAG` 72 行で、想定どおり ### PR2.1 をロールバックするとき 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 07450166c..cf420dab5 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 @@ -36,7 +36,7 @@ AOJ の API は 2026-09-07 に別のホストへ移り、旧ホストは 410 を PR2(#4220)で、合宿系と模擬地区も記号になり、合宿系の title は素になった。管理画面の取り込みも、JAG は全 round で記号を付ける(staging に反映。本番は未反映)。 -PR2.1(#4222)で、AtCoder 上の JAG 4 コンテスト用に `ATCODER_JAG` を新設し、seed に 4 コンテストと 2025 / 2026 の夏合宿を足した(staging に反映。staging / 本番へのデータの投入は未)。 +PR2.1(#4222)で、AtCoder 上の JAG 4 コンテスト用に `ATCODER_JAG` を新設し、seed に 4 コンテストと 2025 / 2026 の夏合宿を足した(staging に反映し、データも投入した。本番は未反映)。 ## PR の分割と順序 From 30578a7e987f8da0816b143b244ee562895361f6 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 06:11:01 +0000 Subject: [PATCH 3/5] feat: add per-contest tables for the JAG camp and regional contests Co-Authored-By: Claude Sonnet 5.5 --- .../plan-pr2-5.md | 75 ------- .../contest-table/aoj_jag_providers.test.ts | 196 +++++++++++++++++- .../utils/contest-table/aoj_jag_providers.ts | 44 ++++ .../aoj_jag_regional_entries.test.ts | 21 ++ .../contest-table/aoj_jag_regional_entries.ts | 87 ++++++++ .../contest_table_provider_groups.test.ts | 48 +++++ .../contest_table_provider_groups.ts | 22 +- 7 files changed, 416 insertions(+), 77 deletions(-) delete mode 100644 docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md create mode 100644 src/features/tasks/utils/contest-table/aoj_jag_regional_entries.test.ts create mode 100644 src/features/tasks/utils/contest-table/aoj_jag_regional_entries.ts diff --git a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md b/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md deleted file mode 100644 index 8f39e41c2..000000000 --- a/docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2-5.md +++ /dev/null @@ -1,75 +0,0 @@ -# PR2.5 — 合宿系と JAG 模擬地区のコンテスト別のテーブルを作る - -前提と共通の決定は [plan.md](./plan.md)。並び順の一覧は [investigation.md](./investigation.md#表示順76-コンテスト)。 - -**状態**: 未着手。PR2 の後に出す(先に出すと、問題 ID の index と `'2389. …'` の title が表に出る)。PR2.1 とは前後してよい。 - -## 何を作るか - -- 1 つのグループ(グループ名・ボタン名とも「JAG 模擬地区」)に、合宿系 50・模擬地区 16・PR2.1 の 10 の計 76 コンテストを、コンテストごとのテーブルとして並べる(M15) -- 並びはスプレッドシートの行順(新しいものが上)。contest_id からは導けないので、明示的な配列で持つ。例: 2014 は 夏合宿 Day2 → 模擬地区(夏合宿の Day3 として開催)→ 夏合宿 Day4。冬合宿 2010 は 夏合宿 2011 の後 -- テーブル名は `getContestNameLabel(contestId)` に括弧書きを付ける(M16) - -| 例 | 括弧書き | -| ---------------------------------------------- | ------------------------------------------ | -| `JAG 夏合宿 2024 Day2(有志)` | `有志` | -| `JAG 春合宿 2015(JAG)` | `JAG` | -| `JAG 模擬地区 2016(in JAG 夏合宿 2016 Day3)` | `in JAG 夏合宿 2016 Day3`(2014 も同じ形) | -| `JAG 夏合宿 2015 Day3` | なし(in AtCoder・過去問) | - -- 出典欄は変えない。この配列は #4018 でコンテスト情報を DB に移すときの移行元になる -- 配列には PR2.1 の 10 件も入れる。データが無い間は表示されないので、PR2.1 より先に出してよい(contest_id は確定済み。[done.md](./done.md#何をしたか)) -- Provider の `contestType` は全件 `AOJ_JAG` にする。provider key の名前空間として使うだけで、`getTaskTableHeaderName` での違いは ABC の H/Ex だけなので、in AtCoder の 4 件も同じでよい。`ATCODER_JAG` に依存しないので、PR2.1 と順序を問わない - -関係する決定: M15 / M16([plan.md](./plan.md#決定事項))。 - -## タスク - -| # | 内容 | -| ----- | ----------------------------------------------------------------------------------------- | -| 2.5-1 | 並び順と括弧書きの配列(76 件) | -| 2.5-2 | Provider クラス(1 コンテスト = 1 テーブル) | -| 2.5-3 | グループ「JAG 模擬地区」を、`contestTableProviderGroups` の `aojJagPrelim` の後に登録する | -| 2.5-4 | テスト(Provider とグループ) | - -`add-contest-table-provider` skill の Layer 4–5 に当たる(Pattern 4 の変形: コンストラクタの引数がコンテスト 1 つ分)。DB を変えず共有インターフェイスも変えないので、クロスレビューの条件には当たらない。 - -## 契約 - -```typescript -// src/features/tasks/utils/contest-table/jag_contest_table_entries.ts -export type JagContestTableEntry = { - contestId: string; - // Bracketed note after the contest name: 'JAG', '有志' or 'in JAG 夏合宿 2016 Day3'. - note?: string; -}; - -export type JagContestTableEntries = JagContestTableEntry[]; - -// Newest first, following the maintainer's spreadsheet; contest ids cannot give this order. -export const JAG_CONTEST_TABLE_ENTRIES: Readonly; - -// src/features/tasks/utils/contest-table/aoj_jag_providers.ts -export class JagContestProvider extends ContestTableProviderBase { - // section = contestId, so the provider key is e.g. AOJ_JAG::JAGSummer2024-day2. - constructor(contestType: ContestType, entry: JagContestTableEntry); - // filter: contest_id === entry.contestId - // getHeaderIdsForTask: sortAojHeaderIds / getDisplayConfig: buildAojDisplayConfig - // getMetadata(): { title: name + (note), abbreviationName: contestId, titleStyle: AOJ_TITLE_STYLE } -} -``` - -## テスト - -- グループの `getSize()` が 76、先頭と末尾のコンテスト -- 2014 の 3 件(夏合宿 Day2 → 模擬地区 → 夏合宿 Day4)の並び -- テーブル名の括弧書き 3 種(`JAG` / `in …` / なし) - -## 却下した方針 - -| 案 | 却下理由 | -| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| 合宿系と模擬地区を 2 グループに分ける | 同じ年の中の前後関係(例: 模擬地区 2014 は夏合宿 2014 の Day3 として開催)を表せない | -| 既存の「JAG 模擬国内」グループに統合する | 模擬国内は年のループで完成している。統合すると、それも明示的な配列に置き換えることになる | -| テーブル名をスプレッドシートの名称(春コンテスト / 冬コンテスト / 模擬アジア地区)にする | 出典欄(`JAG_TRANSLATIONS`)と名称が食い違う([名称の違い](./investigation.md#名称の違い)) | -| 出典欄も PR2.5 でそろえる | ラベルを使う他の画面(検索・投票・タグなど)と fixture に波及する。#4018 でまとめて行う | diff --git a/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts b/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts index 3f29971cc..9489066bc 100644 --- a/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts +++ b/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect } from 'vitest'; import { ContestType } from '#lib/contests/types/contest.js'; import type { TaskResults } from '#lib/types/task.js'; -import { JagPrelimProvider } from './aoj_jag_providers'; +import { JagPrelimProvider, JagRegionalProvider } from './aoj_jag_providers'; const createProvider = (year: number, suffix: '' | 'A' | 'B' = '') => new JagPrelimProvider(ContestType.AOJ_JAG, year, suffix); @@ -423,3 +423,197 @@ describe('JagPrelimProvider', () => { }); }); }); + +// JAGSummer2024-day2: 7 problems (A–G), set by volunteers (有志). +const tasksSummer2024Day2: TaskResults = [ + { + contest_id: 'JAGSummer2024-day2', + task_id: '3623', + task_table_index: 'A', + title: 'Add Add Add', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3624', + task_table_index: 'B', + title: 'Broken Parentheses', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3625', + task_table_index: 'C', + title: 'Convenient Banknotes', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3626', + task_table_index: 'D', + title: 'Do Make Segment Tree', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3627', + task_table_index: 'E', + title: 'Expression Sum', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3628', + task_table_index: 'F', + title: 'Flip Path on Rooted Tree', + }, + { + contest_id: 'JAGSummer2024-day2', + task_id: '3629', + task_table_index: 'G', + title: 'Give Me a Lot of Triangles', + }, +] as TaskResults; + +// jag2018summer-day2 is held on AtCoder, so its titles carry the letter prefix. +const tasksAtCoderSummer2018Day2: TaskResults = [ + { + contest_id: 'jag2018summer-day2', + task_id: 'jag2018summer_day2_a', + task_table_index: 'A', + title: 'A. 10^N+7', + }, + { + contest_id: 'jag2018summer-day2', + task_id: 'jag2018summer_day2_b', + task_table_index: 'B', + title: 'B. Coins', + }, +] as TaskResults; + +describe('JagRegionalProvider', () => { + const providerSummer2024Day2 = new JagRegionalProvider(ContestType.AOJ_JAG, { + contestId: 'JAGSummer2024-day2', + note: '有志', + }); + const providerAtCoderSummer2018Day2 = new JagRegionalProvider(ContestType.ATCODER_JAG, { + contestId: 'jag2018summer-day2', + note: '有志', + }); + + describe('filter', () => { + describe('successful cases', () => { + test('returns only tasks belonging to the given contest', () => { + const mixedTasks = [ + ...tasksSummer2024Day2, + ...tasksAtCoderSummer2018Day2, + ...tasks2023, + ] as TaskResults; + const filtered = providerSummer2024Day2.filter(mixedTasks); + + expect(filtered).toHaveLength(7); + expect(filtered.every((task) => task.contest_id === 'JAGSummer2024-day2')).toBe(true); + }); + + test('returns tasks of a contest held on AtCoder', () => { + const mixedTasks = [...tasksSummer2024Day2, ...tasksAtCoderSummer2018Day2] as TaskResults; + + expect(providerAtCoderSummer2018Day2.filter(mixedTasks)).toEqual( + tasksAtCoderSummer2018Day2, + ); + }); + }); + + describe('edge cases', () => { + test('returns empty array for empty input', () => { + expect(providerSummer2024Day2.filter([] as TaskResults)).toEqual([]); + }); + }); + }); + + describe('getProviderKey', () => { + test('uses the contest id as the section', () => { + expect(providerSummer2024Day2.getProviderKey()).toBe('AOJ_JAG::JAGSummer2024-day2'); + }); + + test('keeps the given contest type for a contest held on AtCoder', () => { + expect(providerAtCoderSummer2018Day2.getProviderKey()).toBe( + 'ATCODER_JAG::jag2018summer-day2', + ); + }); + }); + + describe('getMetadata', () => { + test.each([ + { + contestType: ContestType.AOJ_JAG, + entry: { contestId: 'JAGSummer2024-day2', note: '有志' }, + expected: 'JAG 夏合宿 2024 Day2(有志)', + }, + { + contestType: ContestType.AOJ_JAG, + entry: { contestId: 'JAGSpring2015', note: 'JAG' }, + expected: 'JAG 春合宿 2015(JAG)', + }, + { + contestType: ContestType.AOJ_JAG, + entry: { contestId: 'JAGRegional2016', note: 'in JAG 夏合宿 2016 Day3' }, + expected: 'JAG 模擬地区 2016(in JAG 夏合宿 2016 Day3)', + }, + { + contestType: ContestType.ATCODER_JAG, + entry: { contestId: 'jag2015summer-day3' }, + expected: 'JAG 夏合宿 2015 Day3', + }, + ])('title of $entry.contestId is "$expected"', ({ contestType, entry, expected }) => { + const provider = new JagRegionalProvider(contestType, entry); + + expect(provider.getMetadata().title).toBe(expected); + expect(provider.getContestRoundLabel(entry.contestId)).toBe(expected); + }); + + test('returns the contest id as abbreviationName', () => { + expect(providerSummer2024Day2.getMetadata().abbreviationName).toBe('JAGSummer2024-day2'); + }); + + test('returns titleStyle with text-xl font size, font-bold weight, pb-1 gap, h2 heading tag', () => { + expect(providerSummer2024Day2.getMetadata().titleStyle).toEqual({ + headingTag: 'h2', + fontSize: 'text-xl', + fontWeight: 'font-bold', + bottomGap: 'pb-1', + }); + }); + }); + + describe('getDisplayConfig', () => { + test('hides the header and shows the letter before each title', () => { + expect(providerSummer2024Day2.getDisplayConfig()).toMatchObject({ + isShownHeader: false, + isShownRoundLabel: false, + isShownTaskIndex: true, + columnWrapThreshold: 6, + }); + }); + }); + + describe('getHeaderIdsForTask', () => { + test('returns letters in ascending order regardless of input order', () => { + const reversedTasks = [...tasksSummer2024Day2].reverse() as TaskResults; + + expect(providerSummer2024Day2.getHeaderIdsForTask(reversedTasks)).toEqual([ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + ]); + }); + }); + + describe('generateTable', () => { + test('keeps AtCoder titles as stored, leaving the prefix to the cell', () => { + const table = providerAtCoderSummer2018Day2.generateTable(tasksAtCoderSummer2018Day2); + + expect(table['jag2018summer-day2']['A'].title).toBe('A. 10^N+7'); + expect(table['jag2018summer-day2']['B'].title).toBe('B. Coins'); + }); + }); +}); diff --git a/src/features/tasks/utils/contest-table/aoj_jag_providers.ts b/src/features/tasks/utils/contest-table/aoj_jag_providers.ts index 703948708..1a8cfff92 100644 --- a/src/features/tasks/utils/contest-table/aoj_jag_providers.ts +++ b/src/features/tasks/utils/contest-table/aoj_jag_providers.ts @@ -1,5 +1,6 @@ import { ContestType } from '#lib/contests/types/contest.js'; import type { TaskResult, TaskResults } from '#lib/types/task.js'; +import { getContestNameLabel } from '#lib/contests/index.js'; import { type ContestTableMetaData, type ContestTableDisplayConfig, @@ -7,6 +8,7 @@ import { import { ContestTableProviderBase } from './contest_table_provider_base'; import { sortAojHeaderIds, AOJ_TITLE_STYLE, buildAojDisplayConfig } from './aoj_labels'; +import type { AojJagRegionalEntry } from './aoj_jag_regional_entries'; /** * One JAG Domestic Preliminary (JAG 模擬国内) table, keyed by year. @@ -55,3 +57,45 @@ export class JagPrelimProvider extends ContestTableProviderBase { return this.suffix ? `JAG 模擬国内 ${this.year} ${this.suffix}` : `JAG 模擬国内 ${this.year}`; } } + +/** + * One table per JAG camp or regional contest (JAG 模擬地区 group), including those held on AtCoder. + */ +export class JagRegionalProvider extends ContestTableProviderBase { + private readonly entry: AojJagRegionalEntry; + + constructor(contestType: ContestType, entry: AojJagRegionalEntry) { + super(contestType, entry.contestId); // provider key: AOJ_JAG::JAGSummer2024-day2 + this.entry = entry; + } + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => taskResult.contest_id === this.entry.contestId; + } + + override getHeaderIdsForTask(filtered: TaskResults): string[] { + return sortAojHeaderIds(filtered); + } + + getMetadata(): ContestTableMetaData { + return { + title: this.buildTitle(), + abbreviationName: this.entry.contestId, + titleStyle: AOJ_TITLE_STYLE, + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return buildAojDisplayConfig(); + } + + getContestRoundLabel(_contestId: string): string { + return this.buildTitle(); + } + + private buildTitle(): string { + const name = getContestNameLabel(this.entry.contestId); + + return this.entry.note ? `${name}(${this.entry.note})` : name; + } +} diff --git a/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.test.ts b/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.test.ts new file mode 100644 index 000000000..e7733d166 --- /dev/null +++ b/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.test.ts @@ -0,0 +1,21 @@ +import { describe, test, expect } from 'vitest'; + +import { tasks } from '../../../../../prisma/tasks'; + +import { AOJ_JAG_REGIONAL_ENTRIES } from './aoj_jag_regional_entries'; + +// JAG camps and regionals on AOJ, plus the JAG contests held on AtCoder (e.g. jag2018summer-day2). +const JAG_CONTEST_ID_PATTERN = /^(JAG(Summer|Winter|Spring|Regional)|jag\d{4})/; + +describe('AOJ_JAG_REGIONAL_ENTRIES', () => { + test('lists every JAG camp, regional and AtCoder JAG contest in the seed exactly once', () => { + const seedContestIds = new Set( + tasks + .map((task) => task.contest_id) + .filter((contestId) => JAG_CONTEST_ID_PATTERN.test(contestId)), + ); + const entryContestIds = AOJ_JAG_REGIONAL_ENTRIES.map((entry) => entry.contestId); + + expect(entryContestIds.toSorted()).toEqual([...seedContestIds].toSorted()); + }); +}); diff --git a/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.ts b/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.ts new file mode 100644 index 000000000..1621870ca --- /dev/null +++ b/src/features/tasks/utils/contest-table/aoj_jag_regional_entries.ts @@ -0,0 +1,87 @@ +export type AojJagRegionalEntry = { + contestId: string; + // Bracketed note after the contest name: 'JAG', '有志' or 'in JAG 夏合宿 2016 Day3'. + note?: string; +}; + +export type AojJagRegionalEntries = AojJagRegionalEntry[]; + +// Newest first, following the maintainer's spreadsheet; contest ids cannot give this order. +export const AOJ_JAG_REGIONAL_ENTRIES: Readonly = [ + { contestId: 'JAGSummer2026-day1', note: 'JAG' }, + { contestId: 'JAGSummer2026-day2', note: '有志' }, + { contestId: 'JAGSummer2026-day3', note: 'JAG' }, + { contestId: 'JAGSummer2025-day1', note: '有志' }, + { contestId: 'JAGSummer2025-day2', note: 'JAG' }, + { contestId: 'JAGSummer2025-day3', note: '有志' }, + { contestId: 'JAGSummer2024-day2', note: '有志' }, + { contestId: 'JAGSummer2024-day3', note: 'JAG' }, + { contestId: 'JAGSummer2023-day2', note: '有志' }, + { contestId: 'JAGSummer2023-day3', note: 'JAG' }, + { contestId: 'JAGRegional2022', note: 'JAG' }, + { contestId: 'JAGRegional2021', note: 'JAG' }, + { contestId: 'JAGRegional2020', note: 'JAG' }, + { contestId: 'JAGSummer2019-day1', note: 'JAG' }, + { contestId: 'JAGSummer2019-day2', note: '有志' }, + { contestId: 'JAGSummer2019-day3', note: '有志' }, + { contestId: 'jag2018summer-day2', note: '有志' }, + { contestId: 'JAGSummer2018-day3', note: 'JAG' }, + { contestId: 'JAGRegional2017', note: 'JAG' }, + { contestId: 'jag2017summer-day1', note: '有志' }, + { contestId: 'JAGSummer2017-day3', note: 'JAG' }, + { contestId: 'JAGRegional2016', note: 'in JAG 夏合宿 2016 Day3' }, + { contestId: 'JAGRegional2015', note: 'JAG' }, + { contestId: 'JAGSummer2015-day2', note: '有志' }, + { contestId: 'jag2015summer-day3' }, + { contestId: 'JAGSummer2015-day4', note: 'JAG' }, + { contestId: 'JAGSpring2015', note: 'JAG' }, + { contestId: 'JAGSummer2014-day2', note: '有志' }, + { contestId: 'JAGRegional2014', note: 'in JAG 夏合宿 2014 Day3' }, + { contestId: 'JAGSummer2014-day4', note: '有志' }, + { contestId: 'JAGSpring2014', note: 'JAG' }, + { contestId: 'JAGRegional2013', note: 'JAG' }, + { contestId: 'jag2013summer-warmingup' }, + { contestId: 'JAGSummer2013-day2', note: '有志' }, + { contestId: 'JAGSummer2013-day3', note: '有志' }, + { contestId: 'JAGSummer2013-day4', note: 'JAG' }, + { contestId: 'JAGSpring2013', note: 'JAG' }, + { contestId: 'JAGRegional2012', note: 'JAG' }, + { contestId: 'JAGSummer2012-day2', note: '有志' }, + { contestId: 'JAGSummer2012-day3A', note: '有志' }, + { contestId: 'JAGSummer2012-day3B', note: '有志' }, + { contestId: 'JAGSummer2012-day4', note: 'JAG' }, + { contestId: 'JAGSpring2012', note: 'JAG' }, + { contestId: 'JAGWinter2011', note: 'JAG' }, + { contestId: 'JAGRegional2011', note: 'JAG' }, + { contestId: 'JAGSummer2011-day2', note: '有志' }, + { contestId: 'JAGSummer2011-day3', note: 'JAG' }, + { contestId: 'JAGSummer2011-day4', note: '有志' }, + { contestId: 'JAGWinter2010-day3', note: '有志' }, + { contestId: 'JAGWinter2010-day4', note: 'JAG' }, + { contestId: 'JAGRegional2010', note: 'JAG' }, + { contestId: 'JAGSummer2010-day2', note: '有志' }, + { contestId: 'JAGSummer2010-day3', note: '有志' }, + { contestId: 'JAGSummer2010-day4', note: 'JAG' }, + { contestId: 'JAGWinter2009', note: 'JAG' }, + { contestId: 'JAGRegional2009', note: 'JAG' }, + { contestId: 'JAGSummer2009-day2', note: 'JAG' }, + { contestId: 'JAGSummer2009-day3', note: 'JAG' }, + { contestId: 'JAGWinter2008-day2', note: 'JAG' }, + { contestId: 'JAGWinter2008-day3', note: 'JAG' }, + { contestId: 'JAGRegional2008', note: 'JAG' }, + { contestId: 'JAGSummer2008-day2', note: 'JAG' }, + { contestId: 'JAGSummer2008-day4', note: 'JAG' }, + { contestId: 'JAGWinter2007-day2', note: 'JAG' }, + { contestId: 'JAGWinter2007-day3', note: 'JAG' }, + { contestId: 'JAGRegional2007', note: 'JAG' }, + { contestId: 'JAGSummer2007-day1', note: 'JAG' }, + { contestId: 'JAGSummer2007-day2', note: 'JAG' }, + { contestId: 'JAGSummer2007-day3', note: 'JAG' }, + { contestId: 'JAGWinter2006-day2', note: 'JAG' }, + { contestId: 'JAGWinter2006-day3', note: 'JAG' }, + { contestId: 'JAGRegional2006', note: 'JAG' }, + { contestId: 'JAGSummer2006-day1', note: 'JAG' }, + { contestId: 'JAGSummer2006-day2', note: 'JAG' }, + { contestId: 'JAGSummer2006-day3', note: 'JAG' }, + { contestId: 'JAGRegional2005', note: 'JAG' }, +]; 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 ce604c9e1..4136dc24f 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 @@ -267,4 +267,52 @@ describe('prepareContestProviderPresets', () => { expect(group.getProvider(ContestType.AOJ_JAG, '2016B')).toBeDefined(); }); }); + + describe('explicit-order presets', () => { + const getProviderKeys = () => + presets + .AojJagRegional() + .getAllProviders() + .map((provider) => provider.getProviderKey()); + + test('AojJagRegional registers one provider per contest, newest first', () => { + const group = presets.AojJagRegional(); + const providerKeys = getProviderKeys(); + + expect(group.getGroupName()).toBe('JAG 模擬地区'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'JAG 模擬地区', + ariaLabel: 'Filter JAG Regional and Training Camps', + }); + expect(group.getSize()).toBe(76); + expect(providerKeys.at(0)).toBe('AOJ_JAG::JAGSummer2026-day1'); + expect(providerKeys.at(-1)).toBe('AOJ_JAG::JAGRegional2005'); + }); + + test('AojJagRegional places the 2014 regional between Day2 and Day4 of that summer camp', () => { + const providerKeys = getProviderKeys(); + const day2Index = providerKeys.indexOf('AOJ_JAG::JAGSummer2014-day2'); + + expect(providerKeys.slice(day2Index, day2Index + 3)).toEqual([ + 'AOJ_JAG::JAGSummer2014-day2', + 'AOJ_JAG::JAGRegional2014', + 'AOJ_JAG::JAGSummer2014-day4', + ]); + }); + + test('AojJagRegional registers the contests held on AtCoder under ATCODER_JAG', () => { + const group = presets.AojJagRegional(); + const atCoderContestIds = [ + 'jag2018summer-day2', + 'jag2017summer-day1', + 'jag2015summer-day3', + 'jag2013summer-warmingup', + ]; + + for (const contestId of atCoderContestIds) { + expect(group.getProvider(ContestType.ATCODER_JAG, contestId)).toBeDefined(); + } + expect(group.getProvider(ContestType.AOJ_JAG, 'JAGSummer2015-day4')).toBeDefined(); + }); + }); }); 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 1e8d772ae..19dbabd17 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 @@ -1,4 +1,5 @@ import { ContestType } from '#lib/contests/types/contest.js'; +import { classifyContest } from '#lib/contests/index.js'; import { ABSProvider } from './abs_provider'; import { @@ -35,7 +36,8 @@ import { JOISemiFinalRoundProvider, } from './joi_providers'; import { AojIcpcPrelimProvider, AojIcpcRegionalProvider } from './aoj_icpc_providers'; -import { JagPrelimProvider } from './aoj_jag_providers'; +import { JagPrelimProvider, JagRegionalProvider } from './aoj_jag_providers'; +import { AOJ_JAG_REGIONAL_ENTRIES } from './aoj_jag_regional_entries'; import { ContestTableProviderGroup } from './contest_table_provider_group'; export const ICPC_PRELIM_OLDEST_YEAR = 1998; @@ -330,6 +332,23 @@ export const prepareContestProviderPresets = () => { return group; }, + + AojJagRegional: () => { + const group = new ContestTableProviderGroup('JAG 模擬地区', { + buttonLabel: 'JAG 模擬地区', + ariaLabel: 'Filter JAG Regional and Training Camps', + }); + // addProvider order = display order (first = top). + for (const entry of AOJ_JAG_REGIONAL_ENTRIES) { + const contestType = + classifyContest(entry.contestId) === ContestType.ATCODER_JAG + ? ContestType.ATCODER_JAG + : ContestType.AOJ_JAG; + group.addProvider(new JagRegionalProvider(contestType, entry)); + } + + return group; + }, }; }; @@ -358,6 +377,7 @@ export const contestTableProviderGroups = { aojIcpcPrelim: presets.AojIcpcPrelim(), aojIcpcRegional: presets.AojIcpcRegional(), aojJagPrelim: presets.AojJagPrelim(), + aojJagRegional: presets.AojJagRegional(), }; export type ContestTableProviderGroups = keyof typeof contestTableProviderGroups; From c92661cd93030b00e7a6be71260a2092b58c123d Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 06:20:16 +0000 Subject: [PATCH 4/5] docs: fold the PR2.5 plan into the decision summary Co-Authored-By: Claude Opus 5.5 --- .../task-title-index-change-impact/done.md | 35 +++++++++++++------ .../investigation.md | 2 +- .../task-title-index-change-impact/plan.md | 24 +++++++------ 3 files changed, 39 insertions(+), 22 deletions(-) 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 83908a21d..e5fe732db 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,4 +1,4 @@ -# 完了分(PR0〜PR2.1)— 意思決定の要約 +# 完了分(PR0〜PR2.5)— 意思決定の要約 コードと git の履歴から読み取れない判断と事実だけを、項目ごとにまとめる。変更したファイルや関数は各 PR の差分を見る。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。 @@ -9,6 +9,7 @@ | PR1.5(#4219) | AOJ API の移転に追従する(#4028) | 完了 | | PR2(#4220) | JAG 合宿系と JAG 模擬地区(66 / 625)を問題記号にする(#4030) | staging に反映、本番は未反映 | | PR2.1(#4222) | DB に無い 10 コンテストを入れる(#4221) | staging に反映(データも投入済み)、本番は未反映 | +| PR2.5 | 76 コンテストのテーブルを、グループ「JAG 模擬地区」に作る(#4026) | 実装済み、未マージ | ## 残っている作業 @@ -19,6 +20,9 @@ - PR2.1: 本番(`main` への昇格後)で、デプロイが終わってから 4 コンテストを取り込み、PR2 の移行が通った後に CSV を投入する(staging と同じ手順) - PR2.1 の CSV には `updated_at` を含める(`@updatedAt` は Prisma 側の実装で、DB に既定値が無い。`created_at` / `grade` / `atcoder_problems_difficulty` には既定値がある)。投入は `invalidateTaskCaches()` を経ないので、反映はキャッシュの TTL(1 時間)の後になる - PR1: コンテストテーブルのスクリーンショットでの目視確認 +- PR2.5: staging で「JAG 模擬地区」の表示を目視で確認する(並び、括弧書き、AtCoder の 4 コンテストのセルのリンク先)。開発サーバーでの表示は未確認 +- PR2.5: 本番では、PR2 の移行が通ってから表に出ること。移行の前に出ると、合宿系の問題 ID の index と `'2389. …'` の title が表に出る +- PR2.5: 本番で PR2.1 のデータ(4 コンテストの取り込みと CSV)を入れるまでは、「JAG 模擬地区」に 10 コンテストの見出しと空の枠が出る(下の残タスク) ### 残タスク @@ -26,6 +30,8 @@ PR2 以降の計画には含めていない。 - 2016 年の JAG 模擬国内を API から取り込むと、記号が seed と食い違う(Codex / CodeRabbit)。直すなら、取り込みで 2 回開催を `A` / `B` の `contest_id` に分け、それぞれ A から振る。`getContests` 側もそろえる - seed 整合テストの期待値を `assignProblemLetters` で作っている(PR1 と PR2.1 の Codex)。testing.md の「期待値は literal」に合わない。PR2 では補正したコンテストだけを literal にする(M18)。全件を literal にするかは未判断 +- `TaskTable.svelte` は、データの無い Provider でも見出しとテーブルの枠を描画する(PR2.5 の Codex)。`prepareContestTable` が `null` を返して消えるのは行だけ。直すなら、見出しから枠までを `{#if contestTable}` で囲む(全グループに効く)。既存の挙動なので PR2.5 では直さなかった(メンテナの判断) +- テーブル名・出典欄の名称(春合宿 / 冬合宿 / 模擬地区)を、スプレッドシートの名称(春コンテスト / 冬コンテスト / 模擬アジア地区)にそろえるかは #4018(コンテスト情報の DB 化)で決める。PR2.5 の配列(並びと括弧書き)が移行元になる([名称の違い](./investigation.md#名称の違い)) - `src/lib/services/tasks.ts` の `title.replace(task_table_index, …)` は、AOJ の問題に ContestTaskPair を作るとタイトル本文を置換する(Codex)。AOJ の pair は現在 0 件。直すときは先頭の `${index}. ` だけを置換し、`Amidakuji` のような本文と `A. Title` の両方を回帰テストに含める ## 何をしたか @@ -40,6 +46,7 @@ PR2 以降の計画には含めていない。 - 管理画面の取り込みで、JAG は全 round(模擬国内 / 模擬地区)で記号を付けるようにした - **PR2.1**: ContestType `ATCODER_JAG` を新設し、AtCoder 上の JAG 4 コンテスト(`jag2018summer-day2` / `jag2017summer-day1` / `jag2015summer-day3` / `jag2013summer-warmingup`)を完全一致で分類するようにした。マイグレーションは `prisma/migrations/20261025030756_add_atcoder_jag_to_contest_type/` - seed に、この 4 コンテスト(42 問)と、2025 / 2026 の JAG 夏合宿(`JAGSummer2025-day1`〜`JAGSummer2026-day3`、AOJ 3648–3719、6 コンテスト / 72 問、記号 A–L・title は素)を足した。値はメンテナから受け取った(AtCoder 側は AtCoder Problems の値) +- **PR2.5**: グループ「JAG 模擬地区」を「JAG 模擬国内」の後に足し、合宿系 50・模擬地区 16・PR2.1 の 10 の計 76 コンテストを、コンテストごとのテーブルにした。並びと括弧書きは `aoj_jag_regional_entries.ts` の配列で持つ。中身は [investigation.md の表示順](./investigation.md#表示順76-コンテスト) と同じ ## 進め方 @@ -95,19 +102,25 @@ PR2 以降の計画には含めていない。 ## 分類と表示 -| 決定 | PR | 理由 | -| ----------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| AOJ ラベルから全角括弧を外す(M11) | PR0 | 出典欄の目標書式ではコンテスト名を括弧で囲まない。ラベルは検索・投票・タグ・取り込み画面・提出状況の更新モーダルでも使われており、その表示も変わるが許容した | -| 表示側の変換と `getTaskLabels` を削除する(M7 / M10) | PR1 | DB が記号を持てば変換は恒等になる。インターフェイスの `getTaskLabels` だけを残す案は、上書きする Provider が 0 になり使われないコードが残るので採らなかった | -| ヘッダーの並びは `localeCompare`(M4) | PR1 | 記号に `Number()` を当てると NaN になり、ソートが効かなくなる | -| `buildTitleWithTaskIndex` は「外してから付ける」 | PR1 | AtCoder 側の title は `A. …` を含むので、付けるだけだと二重になる。title にプレフィックスの無い AtCoder の問題は `abs` の practice_1 だけで(本番も同じ)、ABS は index を表示しない | -| `removeTaskIndexFromTitle` は先頭のプレフィックスだけを消す(レビュー) | PR1 | 記号は title の本文にも現れうる。以前は index が空のとき `Dr. Podboq` → `DrPodboq` のように本文の `. ` まで消していた | -| `ATCODER_JAG` を新設し、4 つを contest_id の完全一致で分類する(M17) | PR2.1 | `jag` の前方一致だと、AtCoder Problems の `JAG2013Spring` / `JAG2014Spring`(AOJ の `JAGSpring2013` / `2014` と同じコンテスト)まで入り、問題が二重になる。`OTHERS` に入れないのはメンテナの判断。`AOJ_JAG` だと出典欄の `(AOJ)` や `getTaskUrl` が AOJ を前提に動く | -| ラベル(出典欄)に(有志)を付けない | PR2.1 | PR2.5 のテーブル名でだけ、2018 Day2 / 2017 Day1 に付ける(M16) | +| 決定 | PR | 理由 | +| ------------------------------------------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AOJ ラベルから全角括弧を外す(M11) | PR0 | 出典欄の目標書式ではコンテスト名を括弧で囲まない。ラベルは検索・投票・タグ・取り込み画面・提出状況の更新モーダルでも使われており、その表示も変わるが許容した | +| 表示側の変換と `getTaskLabels` を削除する(M7 / M10) | PR1 | DB が記号を持てば変換は恒等になる。インターフェイスの `getTaskLabels` だけを残す案は、上書きする Provider が 0 になり使われないコードが残るので採らなかった | +| ヘッダーの並びは `localeCompare`(M4) | PR1 | 記号に `Number()` を当てると NaN になり、ソートが効かなくなる | +| `buildTitleWithTaskIndex` は「外してから付ける」 | PR1 | AtCoder 側の title は `A. …` を含むので、付けるだけだと二重になる。title にプレフィックスの無い AtCoder の問題は `abs` の practice_1 だけで(本番も同じ)、ABS は index を表示しない | +| `removeTaskIndexFromTitle` は先頭のプレフィックスだけを消す(レビュー) | PR1 | 記号は title の本文にも現れうる。以前は index が空のとき `Dr. Podboq` → `DrPodboq` のように本文の `. ` まで消していた | +| `ATCODER_JAG` を新設し、4 つを contest_id の完全一致で分類する(M17) | PR2.1 | `jag` の前方一致だと、AtCoder Problems の `JAG2013Spring` / `JAG2014Spring`(AOJ の `JAGSpring2013` / `2014` と同じコンテスト)まで入り、問題が二重になる。`OTHERS` に入れないのはメンテナの判断。`AOJ_JAG` だと出典欄の `(AOJ)` や `getTaskUrl` が AOJ を前提に動く | +| ラベル(出典欄)に(有志)を付けない | PR2.1 | PR2.5 のテーブル名でだけ、2018 Day2 / 2017 Day1 に付ける(M16) | +| 合宿系・模擬地区・AtCoder 上の JAG を 1 グループにし、テーブルはコンテストごとにする(M15) | PR2.5 | 同じ年の中の前後関係(模擬地区 2014 は夏合宿 2014 の Day3 として開催)を表せるのは 1 グループだけ。既存の「JAG 模擬国内」とは統合しない(年のループで完成しており、統合するとそれも配列に置き換えることになる) | +| 並びは明示的な配列で持つ | PR2.5 | スプレッドシートの行順(新しい順)は contest_id から導けない(冬合宿 2010 は夏合宿 2011 の後、など)。今後の合宿は seed と配列の両方に足す(配列の書き漏れは seed との整合テストで落ちる) | +| テーブル名は `getContestNameLabel` の名称に括弧書きを付け、出典欄は変えない(M16) | PR2.5 | スプレッドシートの名称(春コンテスト / 冬コンテスト / 模擬アジア地区)にすると、出典欄と食い違う。出典欄もそろえると、ラベルを使う検索・投票・タグなどと fixture に波及するので #4018 に回した。「過去問」「in AtCoder」「AMATSUKAZE2025」は括弧ごと書かない | +| Provider の `contestType` は、AtCoder 上の 4 コンテストだけ `ATCODER_JAG`、ほかは `AOJ_JAG` | PR2.5 | 計画では、PR2.1 と順序を問わないように全件 `AOJ_JAG` にしていた。PR2.1 が先に staging に入ったので、実際の分類に合わせた(メンテナの判断)。`contestType` が効くのは provider key と `getTaskTableHeaderName`(違いは ABC の H/Ex だけ)なので、表示は変わらない | +| グループのキーは `aojJagRegional` | PR2.5 | キーは localStorage(`contest_table_providers`)に保存され、後から変えると利用者の選択が消える。`aojJagPrelim` と対にし、配列のファイル・型・定数(`aoj_jag_regional_entries.ts`)とクラス(同じファイルの `JagPrelimProvider` と対の `JagRegionalProvider`)もこの名前にそろえた(メンテナの判断) | 分かったこと: - **表示の回帰**(PR1): 旧コードで適用前のデータを、新コードで適用後のデータを描画し、79 コンテストすべてで列順とセルの文字列が一致した。スクリーンショットでの目視は未実施 +- **Codex のレビュー**(PR2.5): 指摘は 2 件で、上の空の枠(残タスクへ)と、plan.md の「PR2.1 は PR2.5 と前後してよい」が古いこと(直した)。Codex が確かめられなかった 2 点はこちらで確かめた。AtCoder の 4 コンテストのセルは AtCoder に飛ぶ(`getTaskUrl` が AOJ に振るのは大文字の `JAG…` だけ)。title の記号は二重にならない(`buildTitleWithTaskIndex` が先頭の `A. ` を外してから付ける) - **DB の `contest_type`**(PR2.1): 書き込むだけで、表示などで読む箇所は無い(分類は毎回 `contest_id` から引く)。enum の値は `ADD VALUE` で DB 上の末尾に足され、schema.prisma の並びと違うが、並べ替えにも使っていない ## seed とデータの投入 @@ -176,6 +189,7 @@ GROUP BY "contest_type"; | AOJ の fixture は取り直さない | PR1.5 | 応答の形が同じため。取り直すと統計値(`solvedUser` など)や新しい回の追加で差分が出て、fixture に依存するテストの期待値まで変わる | | AOJ のテストは nock のまま、クライアントを本番の定数から作る | PR1.5 | CI で実 API を叩かないために nock でモックするのはメンテナの意図どおりで、そのまま残す。問題は、テストが自前の旧ホストからクライアントを作っていて、定数を間違えても通ることだった | | `nock.disableNetConnect()` を足す | PR1.5 | モックを書き忘れたリクエストが、黙って実 API に飛ぶのを防ぐ | +| 配列の contest_id を、seed の JAG 合宿系・模擬地区・AtCoder 上の JAG と突き合わせる | PR2.5 | 計画のテスト(件数・先頭と末尾・2014 の並び・括弧書き)だけでは、手書きの 76 件の id の誤り(そのテーブルが黙って表示されなくなる)を検出できない(メンテナの判断で追加) | 分かったこと(`verify-test-strength` による変異テスト): @@ -183,6 +197,7 @@ GROUP BY "contest_type"; - **PR1.5**: `AOJ_API_BASE_URL` の 4 つの変異(末尾の `/` を外す、旧ホストに戻す、`/api/v1/` にする、`/api` を外す)は、どれも 26 件すべてのテストで検出された - **PR2**: 6 つの変異をすべて検出した(JAG の記号を PRELIM だけ / ICPC だけにする、補正した記号を基本ルールに戻す、基本ルールのコンテストで A・B を入れ替える、title に問題 ID を付け直す、模擬地区の記号を問題 ID に戻す)。title のテストは、空の title・別の問題の title・問題 ID の付け直しも検出する - **PR2.1**: 9 つの変異をすべて検出した(seed の記号の入れ替え・行の欠落・title の不一致、map の欠落、優先度の入れ替え、`jag` / `jag2` の前方一致、4 つのキーでの前方一致、ラベルの定数化)。4 つのキーでの前方一致は当初見逃しており、Codex の指摘でテストを足した +- **PR2.5**: 11 個の変異をすべて検出した(AtCoder の 4 件を `AOJ_JAG` にする、分類の判定を反転する、括弧を半角にする、括弧書きを常に付ける、`abbreviationName` を定数にする、provider key から contest_id を外す、2014 の模擬地区を Day4 の後に動かす、配列の行の欠落・先頭の行の欠落・id の誤り・id の重複)。テストで並びを見ているのは先頭・末尾・2014 の 3 件だけなので、配列の並びと括弧書きは investigation.md の表と機械的に突き合わせ、76 件すべて一致した ## 開発環境(dev container) 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 d47f9d332..8f124017c 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 @@ -89,7 +89,7 @@ AtCoder Problems の `contests.json` には、ほかに `JAG2013Spring` / `JAG20 ## 表示順(76 コンテスト) -PR2.5 の配列の元。スプレッドシートの行順そのまま(上が新しい)。括弧書きは M16 に従う(「過去問」「in AtCoder」「AMATSUKAZE2025」は書かない)。「DB」列の「無し」は PR2.1 で入るもの、「補正」は上の 8 コンテスト。 +PR2.5 の配列(`aoj_jag_regional_entries.ts`)の元。スプレッドシートの行順そのまま(上が新しい)。括弧書きは M16 に従う(「過去問」「in AtCoder」「AMATSUKAZE2025」は書かない)。「DB」列の「無し」は PR2.1 で入るもの、「補正」は上の 8 コンテスト。 | # | 行 | contest_id | 括弧書き | DB | | --- | ---- | ------------------------------------ | ----------------------- | ---- | 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 cf420dab5..281a0b351 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,14 +1,13 @@ # AOJ ICPC / JAG の `problem_index` を A/B/C 形式へ変更する -**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)・PR2.1(#4222)は staging に反映、本番は未反映 +**日付**: 2026-09-09 作成、2026-10-24 に PR2 以降を再構成/ **状態**: PR0・PR1・PR1.5(#4219)完了。PR2(#4220)・PR2.1(#4222)は staging に反映、本番は未反映。PR2.5 は実装済み、未マージ このファイルは全体像と、PR をまたぐ決定・手順だけを書く。各 PR の作業はそれぞれのファイルにある。 | ファイル | 内容 | | -------------------------------------- | -------------------------------------------------- | -| [plan-pr2-5.md](./plan-pr2-5.md) | PR2.5: コンテスト別のテーブルの作成(Provider) | | [plan-pr3.md](./plan-pr3.md) | PR3: 欠落問題の補完 | -| [done.md](./done.md) | PR0〜PR2.1 の決定の要約と残っている作業 | +| [done.md](./done.md) | PR0〜PR2.5 の決定の要約と残っている作業 | | [investigation.md](./investigation.md) | 根拠データ。PR2 の照合結果、表示順、欠落の一覧など | ## ゴール @@ -38,21 +37,24 @@ PR2(#4220)で、合宿系と模擬地区も記号になり、合宿系の ti PR2.1(#4222)で、AtCoder 上の JAG 4 コンテスト用に `ATCODER_JAG` を新設し、seed に 4 コンテストと 2025 / 2026 の夏合宿を足した(staging に反映し、データも投入した。本番は未反映)。 +PR2.5 で、グループ「JAG 模擬地区」に 76 コンテストのテーブルを作った(実装済み、未マージ)。 + ## PR の分割と順序 -| | 範囲 | コンテスト | 問題数 | 内容 | -| ------------------------ | ------------------------------------------------------- | ---------- | ------------------------- | --------------------------------------------------------------------------- | -| [PR1.5](./done.md) | AOJ の API クライアント | — | — | ベース URL を新しいホストへ変える(#4028)。完了(#4219) | -| [PR2](./done.md) | JAG 夏合宿 / 冬合宿 / 春合宿 + JAG 模擬地区 | 66 | 625 | 既存データを記号にする。staging に反映(#4220) | -| [PR2.1](./done.md) | 2025 / 2026 の夏合宿(AOJ)+ in AtCoder の 4 コンテスト | 10 | 114(スプレッドシート上) | 新しいデータを入れる(`ATCODER_JAG` の新設を含む)。staging に反映(#4222) | -| [PR2.5](./plan-pr2-5.md) | PR2 / PR2.1 の 76 コンテスト | 76 | — | グループ「JAG 模擬地区」に、コンテスト別のテーブルを作る | -| [PR3](./plan-pr3.md) | 欠落問題の補完 | — | — | AOJ のコンテスト一覧に無い問題をどう埋めるか | +| | 範囲 | コンテスト | 問題数 | 内容 | +| -------------------- | ------------------------------------------------------- | ---------- | ------------------------- | ---------------------------------------------------------------------------- | +| [PR1.5](./done.md) | AOJ の API クライアント | — | — | ベース URL を新しいホストへ変える(#4028)。完了(#4219) | +| [PR2](./done.md) | JAG 夏合宿 / 冬合宿 / 春合宿 + JAG 模擬地区 | 66 | 625 | 既存データを記号にする。staging に反映(#4220) | +| [PR2.1](./done.md) | 2025 / 2026 の夏合宿(AOJ)+ in AtCoder の 4 コンテスト | 10 | 114(スプレッドシート上) | 新しいデータを入れる(`ATCODER_JAG` の新設を含む)。staging に反映(#4222) | +| [PR2.5](./done.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 にそろえる必要があったため - **PR2 → PR2.5 は守る**。Provider を先に出すと、合宿系の数値の index と `'2389. …'` の title がそのまま表に出る -- **PR2.1 は PR2 / PR2.5 と前後してよい**。PR2 の移行 SQL は index が問題 ID のままの行しか書き換えず、Provider はデータの無いコンテストを表示しない(`TaskTable.svelte` の `prepareContestTable` が `null` を返す) +- **PR2.1 は PR2 と前後してよい**。PR2 の移行 SQL は index が問題 ID のままの行しか書き換えない +- **PR2.5 のコードは PR2.1 の後に出す**。AtCoder の 4 コンテストの Provider に `ATCODER_JAG` を使い、seed との整合テストも PR2.1 の seed を前提にする(計画では PR2.1 と独立させていたが、実装時に変えた。[理由](./done.md#分類と表示))。本番の PR2.1 のデータ投入は PR2.5 の後でもよいが、投入までは「JAG 模擬地区」に 10 コンテストの見出しと空の枠が出る(`TaskTable.svelte` は、データの無い Provider でも見出しと枠を描画する。[残タスク](./done.md#残タスク)) - **PR3 は独立**。PR2 で記号を補正するので、欠落を後から足しても既存の記号はずれない ### スコープ外 From ee5225fe804a94abb3bc1d71b7cca0af71b4c188 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 25 Oct 2026 07:41:28 +0000 Subject: [PATCH 5/5] docs: document the JAG regional provider and record the local check Co-Authored-By: Claude Sonnet 5.5 --- .../task-title-index-change-impact/done.md | 6 +++- .../how-to-add-contest-table-provider.md | 34 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) 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 e5fe732db..d404545e0 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 @@ -20,7 +20,7 @@ - PR2.1: 本番(`main` への昇格後)で、デプロイが終わってから 4 コンテストを取り込み、PR2 の移行が通った後に CSV を投入する(staging と同じ手順) - PR2.1 の CSV には `updated_at` を含める(`@updatedAt` は Prisma 側の実装で、DB に既定値が無い。`created_at` / `grade` / `atcoder_problems_difficulty` には既定値がある)。投入は `invalidateTaskCaches()` を経ないので、反映はキャッシュの TTL(1 時間)の後になる - PR1: コンテストテーブルのスクリーンショットでの目視確認 -- PR2.5: staging で「JAG 模擬地区」の表示を目視で確認する(並び、括弧書き、AtCoder の 4 コンテストのセルのリンク先)。開発サーバーでの表示は未確認 +- PR2.5: staging で「JAG 模擬地区」の表示を目視で確認する(並び、括弧書き、AtCoder の 4 コンテストのセルのリンク先)。開発サーバーでは、2026-10-25 にメンテナが同じ 3 点を確かめた - PR2.5: 本番では、PR2 の移行が通ってから表に出ること。移行の前に出ると、合宿系の問題 ID の index と `'2389. …'` の title が表に出る - PR2.5: 本番で PR2.1 のデータ(4 コンテストの取り込みと CSV)を入れるまでは、「JAG 模擬地区」に 10 コンテストの見出しと空の枠が出る(下の残タスク) @@ -209,3 +209,7 @@ GROUP BY "contest_type"; - **許可リスト**(PR1.5): 開発サーバーはコンテナの中で動き、外向き通信は許可リスト方式で、新しいホスト(163.143.87.177)が入っていなかった(旧ホストは 163.143.87.219)。サーバーの `fetch` が接続で拒否され、管理画面のアクションが `fail(500)` を返していた。ブラウザはホストの Mac から直接開くので見えていた。`init-firewall.sh` は Dockerfile の `COPY` でイメージに入り、起動時に 1 回だけ DNS で IP を引くので、編集を反映するにはコンテナの再起動ではなく再ビルドが要る。再ビルドの後、2026-10-24 にメンテナがローカルで AOJ から取得できることを確かめた - **古い Prisma Client**(PR2.1): enum を足す前から動いている開発サーバーは、古い Prisma Client を持ったままで、`ATCODER_JAG` の登録が 500 になる。Vite はソースの変更を反映するが `node_modules` は読み込み直さないので、一覧には新しいコンテストが出る。開発サーバーの再起動で直る + +分かったこと(ローカルでテーブルが出ない): + +- **seed の流し直し**(PR2.5): PR2.1 より前に seed したローカル DB には 2025 / 2026 の夏合宿が無く、「JAG 模擬地区」にテーブルが出なかった。seed は既存の行を skip して足りない行だけ入れるので、`pnpm db:seed` を流し直せば入る diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index 518b14db2..c5bb25a23 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -7,7 +7,7 @@ ## 事前確認 - [ ] `ContestType`(`src/lib/contests/types/contest.ts`)— 既存で対応できるか?複数 contest_id を統一表示するなら複合型 -- [ ] `classifyContest()`(`src/lib/contests/utils/classification.ts`)— 新 contest_id に正しい ContestType を返すか。`prefixes.ts` の `ABC_LIKE` / `ARC_LIKE` は完全一致、他グループは前方一致なので、続編(例: `diverta2019-2`)は完全一致グループでは個別にキーを追加する +- [ ] `classifyContest()`(`src/lib/contests/utils/classification.ts`)— 新 contest_id に正しい ContestType を返すか。`prefixes.ts` の `ABC_LIKE` / `ARC_LIKE` / `ATCODER_JAG` / `ATCODER_MAIN_OFFICIAL_ONSITE` は完全一致、他グループは前方一致なので、続編(例: `diverta2019-2`)は完全一致グループでは個別にキーを追加する - [ ] `prisma/tasks.ts` にデータが存在するか(複合型は `prisma/contest_task_pairs.ts` も) - [ ] 実装パターン判定(後述4パターン) - [ ] JOI: 2026 年より `joi{YYYY}ho` → `joi{YYYY}sf`。regex は `(ho|sf)` 対応済み @@ -82,6 +82,18 @@ protected setFilterCondition(): (taskResult: TaskResult) => boolean { AOJ/JAG 固有の詳細(`titleStyle`、同一年2回開催の `suffix` 分割など)は `aoj_icpc_providers.ts` / `aoj_jag_providers.ts` を参照。 +**変形: 明示的な配列で並べる(JAG 模擬地区)** — 並びを contest_id から導けないとき(例: 模擬地区 2014 は夏合宿 2014 の Day2 と Day4 の間)は、コンテストごとのエントリの配列(`aoj_jag_regional_entries.ts`)を持ち、1 エントリ = 1 Provider でインスタンス化する。provider key の section は contest_id(例: `AOJ_JAG::JAGSummer2024-day2`)。新しい回は seed(`prisma/tasks.ts`)と配列の両方に足す。配列の書き漏れは seed との整合テスト(`aoj_jag_regional_entries.test.ts`)で落ちる。 + +#### 注意: JAG 模擬地区は AOJ と AtCoder が混在する + +配列には AOJ のコンテスト(`AOJ_JAG`)と AtCoder 上の 4 コンテスト(`ATCODER_JAG`)が並ぶ。新しい回を足すときは次を確かめる。 + +- **分類**: AOJ は `regexForJag` に一致する contest_id、AtCoder は `prefixes.ts` の `ATCODER_JAG` の完全一致で決まる。AOJ の contest_id がこの regex に合わないと、ラベルが崩れ、リンクも AtCoder に飛ぶ。AtCoder 側を前方一致にしない(AtCoder Problems の `JAG2013Spring` / `JAG2014Spring` は AOJ の `JAGSpring2013` / `2014` と同じコンテストで、問題が二重になる) +- **contestType**: Provider には `classifyContest` の結果を渡す(例: `ATCODER_JAG::jag2018summer-day2`)。効くのは provider key だけで、表示は変わらない +- **title**: AtCoder は `A. ` 付き、AOJ は素。セル(`buildTitleWithTaskIndex`)がどちらにも記号を付け直すので、Provider で title を加工しない +- **データ**: AOJ の合宿系は管理画面から取り込めないので、seed と Supabase の CSV で記号付きで入れる。AtCoder 側は管理画面から取り込む +- **記号**: AOJ は問題 ID の昇順で A, B, C… と振るが、問題が AOJ に無いコンテストではずれる。新しい回はメンテナのスプレッドシートと照合する + ### 補足: 固定セクションによる同一 ContestType 内の共存 同一 `ContestType` で異なるプロバイダを共存させる場合、config の `section` フィールドで provider key を一意化する(例: `AWC::0100`)。AWC は `AWCRangeProvider`(通常回)と `AWCSpecialContestProvider`(特殊回)の2クラスを config で使い分けており、すべて同じ `ContestType.AWC` グループに登録される。新しい AWC 回の追加は `contest_table_provider_groups.ts` で `new AWCRangeProvider(...)` または `new AWCSpecialContestProvider(...)` をインスタンス化するだけでよい。 @@ -127,14 +139,15 @@ AOJ/JAG 固有の詳細(`titleStyle`、同一年2回開催の `suffix` 分割 ### コンストラクタパラメータ型 / regex フィルタ型 -| コンテスト | フィルタ方式 | 備考 | -| --------------- | --------------- | -------------------------------------- | -| ICPC 国内予選 | 年度パラメータ | `AojIcpcPrelimProvider(type, year)` | -| ICPC アジア地区 | 年度パラメータ | `AojIcpcRegionalProvider(type, year)` | -| JAG 模擬国内 | 年度パラメータ | 同一年2回開催は suffix 分割 | -| JOI 一次予選 | regex | `joi{YYYY}yo1{a\|b\|c}` | -| JOI 二次予選 | regex + section | 2020 以降 / 2006-2019 で Provider 分離 | -| JOI 本選 | regex | 2026 年より `ho` → `sf` サフィックス | +| コンテスト | フィルタ方式 | 備考 | +| --------------- | --------------- | ------------------------------------------------------------------------------- | +| ICPC 国内予選 | 年度パラメータ | `AojIcpcPrelimProvider(type, year)` | +| ICPC アジア地区 | 年度パラメータ | `AojIcpcRegionalProvider(type, year)` | +| JAG 模擬国内 | 年度パラメータ | 同一年2回開催は suffix 分割 | +| JAG 模擬地区 | 明示的な配列 | 合宿系・模擬地区・AtCoder 上の JAG の 76 件。`JagRegionalProvider(type, entry)` | +| JOI 一次予選 | regex | `joi{YYYY}yo1{a\|b\|c}` | +| JOI 二次予選 | regex + section | 2020 以降 / 2006-2019 で Provider 分離 | +| JOI 本選 | regex | 2026 年より `ho` → `sf` サフィックス | ### 複合ソース型 @@ -154,6 +167,7 @@ AOJ/JAG 固有の詳細(`titleStyle`、同一年2回開催の `suffix` 分割 3. **contestTypePriorities の JSDoc カテゴリ名** — 4カテゴリ名(`Educational` / `Contests for genius` / `Special contests` / `External platforms`)は変更不可。括弧内の数値範囲のみ変更可 4. **addProvider の順序** — 呼び出し順 = 表示順(先 = 上)。新しい回が上に来るように 5. **分類 regex の拡張** — 新形状の contest_id を追加するとき、regex を広げると `getContestNameLabel`・`AojGenerator.canHandle` にも波及する +6. **データの無い Provider** — `TaskTable.svelte` は、データの無い Provider でも見出しと空の枠を描画する(`prepareContestTable` が `null` を返して消えるのは行だけ)。データを入れる前に Provider を出すと、空の枠が並ぶ --- @@ -170,4 +184,4 @@ AOJ/JAG 固有の詳細(`titleStyle`、同一年2回開催の `suffix` 分割 --- -**最終更新**: 2026-10-13 +**最終更新**: 2026-10-25