Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ allowed_domains=(
vscode.download.prss.microsoft.com
# External APIs the app calls (src/lib/constants/urls.ts)
kenkoooo.com
judgeapi.u-aizu.ac.jp
onlinejudge.u-aizu.ac.jp
# CodeRabbit CLI
cli.coderabbit.ai
app.coderabbit.ai
Expand Down
79 changes: 79 additions & 0 deletions docs/dev-notes/2026-09-09/task-title-index-change-impact/done.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 完了分(PR0 / PR1 / PR1.5)— 意思決定の要約

**状態**: PR0 完了(#4212)・PR1 完了(#4217)・PR1.5 実装済み(ブランチ `#4028`、`471ad20c`)。全体像は [plan.md](./plan.md)、根拠データは [investigation.md](./investigation.md)。

コードと git の履歴から読み取れない判断と事実だけを残す。変更したファイルや関数は #4212 / #4217 と `471ad20c` の差分を見る。PR1.5 は [末尾](#pr15--aoj-api-の移転に追従する4028)。

## 何をしたか

- **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`)は削除した

## 決定と理由

| 決定 | 理由 |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 移行 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 にするかは未判断
- `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 に入った後の JAG REGIONAL の取り込み(PR2 の前提)が、Vercel 上での実地の確認を兼ねる([plan-pr2.md](./plan-pr2.md#着手前にすること))。念のため、こちらからの取得ツールは新しいホストへの接続を拒否され(`ECONNREFUSED`)、旧ホストには届いて 410 を受けた。新しいホストが一部の送信元を拒んでいる可能性があるので、staging で失敗したら接続元の制限を疑う
Loading
Loading