You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
スクリプトには総数のアサーションを入れる。抽出漏れが欠落の誤検出に直結する(事例)。title は大文字小文字・空白・記号を無視して比べ、セルで切れた title は前方一致にする。スプレッドシート自体の誤りは検出できない
PR1 / PR2 のスクリプトは git に残っている: git show 171bf3c9:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_problem_letters.ts、git show c605d3bd:…/generate_aoj_jag_problem_letters.ts。わざと誤りを入れたコピー 5 種類(順番の入れ替え、途中の title の誤り、欠番、問題数の不足、切れた title の前半の誤り)を、どちらも検出した
PR2 の補正 8 コンテスト(45 行)は、コンテストの全行を上書きして 1 つのコンテストに 2 つの規則を混ぜない。各ステップの更新件数を DO ブロックで検査し、想定外なら RAISE する(想定値: 空の DB 0、模擬地区の無い開発 DB 467 / 32 / 467、staging・本番 625 / 45 / 467)。模擬地区が一部だけの DB では RAISE し、エラー文で「全部取り込むか、全部消す」と案内する
順位はコンテストの全行で付け、更新は問題 ID のままの行だけにする(Codex の指摘)。順位を付ける側でも絞ると、記号の行と問題 ID の行が混ざったコンテストで A から振り直してしまう。title から外すのは「自分の task_id + . 」で始まるものだけ(^[0-9]+\. だと番号で始まる本来の title まで削る)
Activity
Merge branch 'staging' of github.com:AtCoder-NoviSteps/AtCoderNoviSte…
Merge branch 'staging' of github.com:AtCoder-NoviSteps/AtCoderNoviSte…
KATO-Hiro commentedon Oct 28, 2026
AOJ ICPC / JAG の
problem_indexを A/B/C 形式へ変更する日付: 2026-09-09 作成、2026-10-28 に PR3 を実装し、全体を再編/ 状態: PR0〜PR2.5 は完了(本番・staging で確認済み)。PR3 は実装・目視確認済み、未マージ。根拠データは investigation.md。
ゴール
problem_index(DB 上はtask_table_index)に AOJ 問題 ID ではなく問題記号を持たせ、表示側が変換なしにそれを使える状態にする。AOJ 3467(ICPC 国内予選 2024)ICPC 国内予選 2024 - F(AOJ)Fを算出task_table_indexのFをそのまま使う現在の状態と PR
AOJ ICPC・JAG の全範囲(ICPC 国内予選 / アジア地区 / JAG 模擬国内 79 コンテスト、合宿系・模擬地区・in AtCoder 76 コンテスト)で
task_table_indexが問題記号になった。title は素(問題 ID も記号も付かない)で、セルはtask_table_indexからA. タイトルを組み立てる。ATCODER_JAGを新設し、AtCoder 上の JAG 4 コンテストと 2025 / 2026 の夏合宿(計 10 コンテスト)を足す(#4222)順序の制約:
'2389. …'の title がそのまま表に出るATCODER_JAGと PR2.1 の seed を前提にする。本番の PR2.1 のデータ投入が PR2.5 より後だと、投入までは「JAG 模擬地区」に 10 コンテストの見出しと空の枠が出る残りのタスク
PR3:
実装・Codex のクロスレビュー(指摘はテストの強化 1 件で反映済み)・開発サーバーでの目視(JAG 冬合宿 2006 Day2 の表と問題一覧で、ダミーのリンク先と
(No Judge)を確認)は済んだ。mainへ昇格した後、本番にも同じ CSV を投入して確かめるPR 外(優先度の低いものを含む):
A/Bのcontest_idに分け、それぞれ A から振る(getContests側もそろえる)assignProblemLettersで作っていて、testing.md の「期待値は literal」に合わない。補正したコンテストだけ literal にしている。全件を literal にするかは未判断TaskTable.svelteは、データの無い Provider でも見出しとテーブルの枠を描画する。直すなら、見出しから枠までを{#if contestTable}で囲む(全グループに効く)。既存の挙動なので触っていないsrc/lib/services/tasks.tsのtitle.replace(task_table_index, …)は、AOJ の問題に ContestTaskPair を作るとタイトル本文を置換する。AOJ の pair は現在 0 件。直すときは先頭の${index}.だけを置換し、Amidakujiのような本文とA. Titleの両方を回帰テストに含めるスコープ外
次の AOJ は数値の index のまま残し、別タスクとする(本番の件数、2026-10-21)。
プログラミング入門 - ITP1_1_A(AOJ)のように不格好task_id::intが使えないUTPC 2010 - 2207(AOJ)AOJ-KUPC2011の 2279 を移行 SQL の対象から外す(index は既にIで正しく、基本ルールではAになる)btrim、46 件の件数チェック付き)で済む3 つの難所
A,B,C…)で復元できる。⇒ 対応表はスクリプトで機械生成できるtask_table_indexを直接読む。⇒ DB の値そのものを直す以外に手段がない決定事項
titleから問題 ID のプレフィックスを除くremoveTaskIndexFromTitleが空振りし、'2389. …'が表に出るJAGSummer2012-day3Aなど)にそろえるアドホックな対応が多く、年 1 回の追加には重い(メンテナの判断)getTaskLabelsを削除するgetTaskLabelsだけを残すと、上書きする Provider が 0 の使われないコードが残るlocaleCompareNumber()を当てると NaN になるbuildTitleWithTaskIndexは先頭の記号を外してから付ける。removeTaskIndexFromTitleは先頭のプレフィックスだけを消すA. …を含むので、付けるだけだと二重になる。本文の.まで消すとDr. Podboq→DrPodboqになるATCODER_JAGを新設し、contest_id の完全一致で分類するjagの前方一致だとJAG2013Spring/JAG2014Spring(AOJ のJAGSpring2013/2014と同じコンテスト)まで入り、二重になる。AOJ_JAGだと(AOJ)やgetTaskUrlが AOJ 前提で動くgetContestNameLabelの名称に括弧書き(JAG/有志/in JAG 夏合宿 20xx Day3)を付け、出典欄は変えないcontestTypeは、AtCoder 上の 4 コンテストだけATCODER_JAG、ほかはAOJ_JAG。グループのキーはaojJagRegionalcontestTypeが効くのは provider key とgetTaskTableHeaderNameだけで、表示は変わらない。キーは localStorage に保存されるので後から変えられないname)と同じ」で見る却下した方針:
prisma/tasks.tsに手書きし seed だけ直す: 取り込みが数値を入れ続け、新旧の書式が混在するaddUsers/addAnswers/addVoteStatisticsDemoDataまで走る(prisma/seed.ts)titleに記号を入れて'A. Peculiar Protocol'に揃える: 同じ情報を 2 列に持つとドリフトする(詳細)PR3: 欠落問題の補完
根拠は investigation.md の 7 章。
AOJ の JAG / ICPC で、公式の問題なのに seed に無い 26 問を足す。管理画面の取り込みは AOJ のコンテスト一覧を使うので取り込めない。seed に足し、staging / 本番はメンテナが Supabase の CSV で入れる。
3643 は 2026-10-10 に AOJ に追加されたばかりで公開前かもしれない(
availableが 4、提出が 0)が、入れる。合宿系は取り込みの対象外なので、一覧に入るのを待っても取り込めない。ダミー(21 問)の決定
task_id:NoJudge-{contest_id}_{記号の小文字}(例:NoJudge-JAGWinter2006-day2_a)。task_table_indexは大文字のまま。接頭辞が「採点の仕組みが無い」という分岐の理由をそのまま表し、AOJ(数字)とも AtCoder(abc300_a)とも構造上ぶつからない。判定はisNoJudgeTask()(startsWith('NoJudge-'))に一本化するWireing Assistantも原文どおり2008/Practice/模擬地区予選、計 5 ページ)。問題文・ジャッジデータ・講評のどれにも下りられる。URL は組み立てず、ブラウザで開けたものをパーセントエンコードのまま貼るNoJudgeGeneratorをAojGeneratorの前に置く(ダミーは JAG のcontest_idを使うので、順序に意味がある)。task_idをキーにした map から引き、漏れていれば空文字を返して AOJ には回さない。seed との突き合わせのテスト 2 件が、map の漏れと打ち間違いを守る(AOJ)ではなく(No Judge)(例:JAG 冬合宿 2006 Day2 - A(No Judge))。addContestNameToTaskIndexに必須のtaskIdを足したLETTERS_OF_SHIFTED_CONTESTSに入れる(Number()で並べられないため、入れ忘れを検出するテストがある)。連番に戻ったJAGRegional2013とJAGSummer2024-day3は外したtask_idを AOJ の ID に UPDATE する。取り込みは後から足された問題へずれた記号を付け、重なると表から黙って消える。外部キー 6 つはON UPDATE CASCADE(手順)採らなかった案
task_id: 数字(100000 番台は AOJ の本物の ID と見分けられず、AOJ が決める領域で衝突が残る。50xx 番台は AOJ の採番がいずれ届く)、Missing-(AOJ にある 5 問にも当てはまり、仮のレコードにも読める)、jag-icpc_など(置き場所を表し、分岐の理由を表さない)問題文とデータセットのページ(講評に辿れない)getTaskUrl()の先頭で map を引く: 判定が「map に載っているか」になり、出典のラベル(接頭辞で判定)と基準が割れる。漏れると開けない AOJ のリンクになるurl列を足す: マイグレーションと呼び出し元 9 ファイルの変更が要り、21 件には重い。他サイトに対応するときに再検討するisNoJudgeTask()をsrc/lib/utils/に置く案は、lib/utils/task.tsが#lib/contests/index.jsを import しているので循環 import になる。lib/contestsに置いたtaskIdを省略できる引数にする: 渡し忘れが黙って(AOJ)のままになる。必須なら型チェックが漏れを見つけるtitle属性のツールチップ(スマホでは出ない。知らせる仕組みが必要になったら最初に検討する)、表のタイトルにnoteで書く(主催者の欄と意味が混ざる)、詳細ページに一文添える(表から直接開く人の目に入らない)staging / 本番への投入(メンテナ)
コードのデプロイの後に CSV を入れる(先に入れると、デプロイまでの間、ダミーが開けない AOJ のリンクと
(AOJ)のラベルで出る)。id・contest_type・created_at・updated_atも明示する。Supabase の CSV は Prisma の既定値を使わず、created_atは DB に既定値があっても空になった例がある(docs/guides/agent-rules/prisma-db.md)idは PR2.1 と同じく、メンテナのスクリプトでsha256(contest_id + title)にする(Task.idを再計算して引く箇所は無い。根拠)invalidateTaskCaches()を経ないので、画面に出るのはタスクのキャッシュの有効期限(1 時間)が切れた後task_idcontest_idcontest_typetask_table_indextitleCSV を入れたら、SQL で確かめる。seed はテストが守るが、CSV は守らない。
(contest_id, task_table_index)には一意制約が無く、同じコンテストで記号が重なると、表で片方の問題が黙って消えるtask_idの打ち間違いが通る。NoJudge-の綴りを誤ると(AOJ)と開けない AOJ のリンクで出て、AOJ の ID を誤ると別の問題に飛ぶ。contest_typeの列を CSV から落とすと、DB の既定値ABCで黙って入る。最後の SQL でprisma/tasks.tsの値(contest_typeはclassifyContest()の結果)と 1 行ずつ突き合わせるcontest_idデータ移行の共通手順
PR2(記号の移行)、PR2.1(enum の追加)、PR3 に当てはまる。
CI での適用
.github/workflows/ci.yml(2026-10-24 に確認)は、build / lint / check / unit-test が通ると、main以外ではpreviewジョブ(適用先は staging の DB)、mainではproductionジョブを走らせる。どちらもvercel buildの後・デプロイの前にprisma migrate deployを流す。previewジョブの条件がgithub.ref != 'refs/heads/main'だけで、PR の ref も当たる。staging へのマージより前であるmainへ昇格した時点で production に適用されるので、staging での確認が自然にゲートになるpreviewジョブの「Assign staging domain」はコメントに「if staging branch」とあるのに条件はgithub.ref != 'refs/heads/main'で、PR のデプロイにも staging のドメインが付け替わる手順
pnpm exec prisma migrate dev --create-only --name <name>、SQL をmigration.sqlに書くUPDATEをSELECTに置き換えて「現在値 → 変更後」を確認する。コンテストの切り替わり目(記号がAに戻る箇所)と問題数の少ない古いコンテストは目視する価値があるpnpm exec prisma migrate devで適用し、表示を確認する(ここまで必ず PR を開く前に)mainへ昇格staging が防げないのは、対応表・欠落判断そのものの誤り。誤った記号でも SQL は完走し、それらしく表示される(難所 2)。
事故対策
task_table_index == task_idが成り立っていたため)。WHERE 句が壊れて AtCoder 系に波及すると一括復元の手段が無くなる(実測)task_table_indexもtitleのプレフィックスもtask_idから再構成できる。再修正時は Supabase Pro の PITR が唯一の手段updated_at:@updatedAtは Prisma Client 側の実装で、生 SQL では更新されない。意図的に含めないsrc/lib/clients/cache.ts)。生 SQL と CSV の投入はinvalidateTaskCaches()を経由しない。「更新したのに表示が変わらない」ときはこれを疑うスプレッドシートとの照合
git show 171bf3c9:docs/dev-notes/2026-09-09/task-title-index-change-impact/generate_aoj_problem_letters.ts、git show c605d3bd:…/generate_aoj_jag_problem_letters.ts。わざと誤りを入れたコピー 5 種類(順番の入れ替え、途中の title の誤り、欠番、問題数の不足、切れた title の前半の誤り)を、どちらも検出した実装中に分かったこと
コードと git から読み取れない事実だけを残す。
移行 SQL(PR1 / PR2)
contest_typeではなくcontest_idの接頭辞で絞る。合宿系も模擬国内と同じAOJ_JAGに分類されるので、PR1 でcontest_typeだけで絞ると合宿系 467 件まで更新する。PR2 は^JAG(Summer|Winter|Spring|Regional)で絞るUPDATEは 0 件でもエラーにならないので、取りこぼしが静かに起きるtask_id+.」で始まるものだけ(^[0-9]+\.だと番号で始まる本来の title まで削る)AOJ-KUPC2011の 2279 は title だけを素にし、index(I)は触らないrow_number()はbigintを返しchr(bigint)は無いので::intが要る取り込みと AOJ API(PR1 / PR1.5)
JAGPrelim20162、2738–2751)があり、取り込みでは A–N になる。seed と本番はJAGPrelim2016A/Bに分かれていて、取り込みは既存の問題を skip するので影響しないlargeCl.idが小文字になっただけ(コードは読まない)new URL(endpoint, base)は、ベースの末尾に/が無いか、エンドポイントの先頭に/があると/apiを落とす。旧ホストはパスが無かったので表に出ていなかったECONNREFUSEDで拒まれたが、Vercel からは問題なく取得できた。問題ページhttps://onlinejudge.u-aizu.ac.jp/problems/{id}は今のまま開けるnock.disableNetConnect()を足した。AOJ の fixture は取り直さない(統計値や新しい回の追加で、fixture に依存する期待値まで変わるため)分類と表示(PR1 / PR2.1 / PR2.5)
getTaskUrlが AOJ に振るのは大文字のJAG…だけ)。title の記号は二重にならないcontest_typeは書き込むだけで、読む箇所は無い(分類は毎回contest_idから引く)。enum の値はADD VALUEで DB 上の末尾に足され、schema.prisma の並びと違うが、並べ替えにも使っていないseed とデータの投入(PR2 / PR2.1)
idは既存の合宿系と同じsha256(contest_id + title)ATCODER_JAG42 行・AOJ_JAG72 行)ATCODER_JAGを読めない。ロールバックの前に該当 42 行をOTHERSに変えて件数を控え、ロールフォワードの後に件数チェック付きの UPDATE で戻す。件数が違えば RAISE させ、期待値を書き換えて通さない(手順の SQL は git の履歴にある)開発環境
.devcontainer/init-firewall.sh)に新しいホストが無かったこと(起動時に 1 回だけ DNS で IP を引くので、反映にはコンテナの再ビルドが要る。旧ホストは残さず置き換えた)と、enum を足す前から動いている開発サーバーが古い Prisma Client を持っていたこと(再起動で直る)pnpm db:seedを流し直せば入るKATO-Hiro commentedon Oct 28, 2026
調査の根拠データと検討の経緯
plan.md の裏付け。単独では読まず、計画から参照される想定。計画の決定が常に優先される。
PR0 / PR1 だけに関わる調査(出典欄の呼び出し元、テストの影響範囲など)は、コードと #4212 / #4217 の差分から読めるので削除した。判断の要約は plan.md。
1. PR2 の照合(2026-10-24)
メンテナのスプレッドシート(スクリーンショット 16 枚、行 2〜1025。上が新しい)と、本番の問題一覧の JAG 模擬地区(スクリーンショット 10 枚、158 問)を目視で突き合わせた。目視の結果で、PR2 の照合スクリプト(書き写したスプレッドシートとの機械的な照合)でも同じ結果になった(plan.md)。
スプレッドシートで折りたたまれた行(40–50 ほか 19 か所)は JAG 模擬国内だけで、合宿系・模擬地区は含まない(メンテナに確認済み)。
対応の確認
記号の補正が要るコンテスト
基本ルール(問題 ID の昇順に A, B, C…)と、スプレッドシートの位置が食い違うコンテスト。いずれも先頭か途中に、AOJ に無い問題がある。末尾だけの欠落(模擬地区 2008 の J)は記号がずれないので含めない。
JAGSummer2006-day1JAGSummer2006-day3JAGWinter2006-day2JAGWinter2006-day3JAGWinter2007-day3JAGSummer2024-day3JAGRegional2005JAGRegional20138 コンテスト / 45 行(合宿系 32、模擬地区 13)。基本ルールから値が変わるのは 32 行(合宿系 21、模擬地区 11)。
DB に無いコンテスト
PR2.1 で入れる。contest_id と問題番号は 2026-10-24 にメンテナから受け取った。
JAGSummer2026-day1JAGSummer2026-day2JAGSummer2026-day3JAGSummer2025-day1JAGSummer2025-day2JAGSummer2025-day3jag2018summer-day2jag2018summer_day2_a〜kjag2017summer-day1jag2017summer_day1_a〜kjag2015summer-day3icpc2015summer_day3_a〜jjag2013summer-warmingupicpc2013summer_warmingUp_a〜j計 114 問(AOJ 72 は各 12 問、AtCoder 42)。いずれも「AOJ 未収録」の表記は無く、受け取った問題名はスプレッドシートと記号の位置ごとに一致した(目視)。AOJ の 6 コンテストは問題 ID が連番で、記号は基本ルールと一致する。
AtCoder Problems の
contests.jsonには、ほかにJAG2013Spring/JAG2014Springもある。AOJ のJAGSpring2013/JAGSpring2014と同じコンテストなので対象外とする。表示順(76 コンテスト)
PR2.5 の配列(
aoj_jag_regional_entries.ts)の元。スプレッドシートの行順そのまま(上が新しい)。括弧書きは plan.md の決定事項に従う(「過去問」「in AtCoder」「AMATSUKAZE2025」は書かない)。「DB」列の「無し」は PR2.1 で入るもの、「補正」は上の 8 コンテスト。JAGSummer2026-day1JAGSummer2026-day2JAGSummer2026-day3JAGSummer2025-day1JAGSummer2025-day2JAGSummer2025-day3JAGSummer2024-day2JAGSummer2024-day3JAGSummer2023-day2JAGSummer2023-day3JAGRegional2022JAGRegional2021JAGRegional2020JAGSummer2019-day1JAGSummer2019-day2JAGSummer2019-day3jag2018summer-day2(AtCoder)JAGSummer2018-day3JAGRegional2017jag2017summer-day1(AtCoder)JAGSummer2017-day3JAGRegional2016JAGRegional2015JAGSummer2015-day2jag2015summer-day3(AtCoder)JAGSummer2015-day4JAGSpring2015JAGSummer2014-day2JAGRegional2014JAGSummer2014-day4JAGSpring2014JAGRegional2013jag2013summer-warmingup(AtCoder)JAGSummer2013-day2JAGSummer2013-day3JAGSummer2013-day4JAGSpring2013JAGRegional2012JAGSummer2012-day2JAGSummer2012-day3AJAGSummer2012-day3BJAGSummer2012-day4JAGSpring2012JAGWinter2011JAGRegional2011JAGSummer2011-day2JAGSummer2011-day3JAGSummer2011-day4JAGWinter2010-day3JAGWinter2010-day4JAGRegional2010JAGSummer2010-day2JAGSummer2010-day3JAGSummer2010-day4JAGWinter2009JAGRegional2009JAGSummer2009-day2JAGSummer2009-day3JAGWinter2008-day2JAGWinter2008-day3JAGRegional2008JAGSummer2008-day2JAGSummer2008-day4JAGWinter2007-day2JAGWinter2007-day3JAGRegional2007JAGSummer2007-day1JAGSummer2007-day2JAGSummer2007-day3JAGWinter2006-day2JAGWinter2006-day3JAGRegional2006JAGSummer2006-day1JAGSummer2006-day2JAGSummer2006-day3JAGRegional2005内訳: 合宿系 50、模擬地区 16、DB に無いもの 10(AOJ 6、AtCoder 4)。
年の中での並び
contest_id から並びを導けない例(スプレッドシートの上から順)。冬合宿・冬コンテストは、年の数字が同じ夏合宿・模擬地区より上にある。
名称の違い
JAG_TRANSLATIONS)JAGSpring2012〜2015JAGWinter2009/JAGWinter2011(日の無いもの)JAGRegional*テーブル名は現行のコードに合わせる。出典欄と同じ名称になり、違うのは括弧書きの有無だけになる。
2. 実測データ
対象規模
prisma/tasks.tsの集計(2026-10-21)と、本番だけにある JAG 模擬地区。PR1 / PR2 の合計は 1,257 問 / 145 コンテスト。PR1 の前は、seed の 1,099 問すべてで
problem_index==task_id(AOJ 問題 ID)だった。本番の件数(2026-10-21)
本番で
contest_typeごとに集計した結果。seed と一致するのはAOJ_ICPCだけだった。AOJ_ICPCAOJ_JAGAOJ_UNIVERSITYAOJ_COURSESAOJ_PCKAOJ_JAGの差(16 コンテスト / 158 問)はすべて JAG 模擬地区(管理画面から取り込める。seed には 0 件)。接頭辞ごとに分けると、ほかはすべて本番と seed で一致した。AtCoder 側(
AOJ_*以外)で title が${task_table_index}.で始まらない問題は、本番でもabsの practice_1 の 1 件だけだった。PR2 の範囲の本番と staging(2026-10-24)
本番と staging で範囲の状態を集計した結果(SQL は
git show 7c68cef6:docs/dev-notes/2026-09-09/task-title-index-change-impact/plan-pr2.mdの「確かめ方」)。Most distant point from the stations\t)。前後に空白のある title はほかにもある(下記)前後に空白のある title
本番で
title <> btrim(title, E' \t\r\n')の行を数えると 46 件あった(2026-10-24)。"Chains of Explosions "(末尾の空白)"Most distant point from the stations\t"(末尾のタブ)"A. Weather Prediction "(末尾の空白)jag_prelim/jag_regionalにも同じ空白がある)。AtCoder Problems も同様trim()するようになった(2026-05-18、0e1ec4e7)。46 件はそれより前に入った行。seed の値は trim 済みremoveTaskIndexFromTitleのstartsWith(先頭のA.を見るだけ)しか無く、完全一致で比べる箇所は無い。検索は部分一致なので、空白で終わる検索語でなければ影響しないidはsha256(contest_id + title)だが、既存行のidは再計算されないので関係ないPR2 では直さず、残タスクとした(plan.md)。
欠落は機械的に検出できない
AOJ 問題 ID の連番性を調べると、seed の 129 コンテスト中 128 が連番。だが欠落が無い証拠にならない。 AOJ に登録されていない問題なら ID も無いため、残った問題の ID は連番のままになる。AOJ に問題はあるのにコンテスト一覧(取り込み元)から外れている場合も同じである。ICPC 国内予選 2002 の E(Enclosing Circles)は AOJ に問題 1123 として存在するが一覧に無く、取り込まれていない。2002 年の A〜D は 1119〜1122 で連番のままであり、1123 の次の 1124 は 2003 年の A である。
連番でない 1 コンテストは
JAGSummer2024-day3(3643 が無い)で、スプレッドシートの G(Interactive String Guessing)に当たる。スプレッドシートでは「AOJ 未収録」だが、AOJ に 3643 はあり、コンテスト一覧から外れているだけだった。データから欠落を示唆するのは問題数の異常の方だが、それも決め手にならない:
JAGSummer2006-day1JAGWinter2006-day2JAGWinter2006-day3JAGSummer2007-day1ICPCPrelim1998/2000/2002titleの持ち方が AOJ 内で割れているtitleの形'Peculiar Protocol''2389. Asynchronous Exceptions''Restriction Enzyme''EX8. たくさんのA足すB問題'プレフィックスは合宿系の 467 件すべてで
task_idと完全に一致していた(不一致 0 件)。 したがって除去も復元も機械的に安全に行える。模擬地区は管理画面の取り込みで本番に入っており、取り込みは導入時(2024-10、
7dcc5e4c)からproblem.nameをそのまま title にしている。範囲の集計でもプレフィックスは 0 行だった(本番・staging)。退行の機序(title を直さずに index だけ記号にした場合):
復元可能性の実測
task_table_index == task_idが例外ゼロで成立(seed、PR1 の前)task_idのサフィックスから導出できるが、216 件は例外本番にある模擬地区 158 件も、範囲の集計で全行
task_table_index == task_idだった(2026-10-24)。導出できない 216 件の内訳:
abc41(abc318_hに対し index はEx)、typical32、APG27(APG4bPython_coに対し index はEX8)、arc27、tdpc20、math-and-algorithm15、ほか(task_idにサフィックスが無い AOJ 大学系 29 件を含む)。abc*_h → Exは規則として書けるため理論上は 90% 超まで導出できるが、 事故対応の手段としては成立しない。理由は 2 つ。task_table_indexを上書きした後では、どの行が元からAだったのかを区別できない既存 DB は自動更新されない
prisma/seed.tsでfindUnique(task_id)→ 既存なら skipsrc/lib/services/tasks.tsのcreateTaskでisExistsTask(task_id)→ 既存なら silent return。classifyContestがnullを返す contest_id も黙って捨てるupdateTaskはgradeしか触らない一意性は壊れない
prisma/schema.prismaの Task で unique なのは 2 つのみ。id(@id @unique)sha256(contest_id + task.id)task_id(@unique)task.idをそのままどちらも入力は contest_id と AOJ 問題 ID のみ。
problem_indexは混ざらない。FK も全てtask_id参照(TaskTag/TaskAnswer/WorkBookTask/VoteGrade/VotedGradeCounter/VotedGradeStatistics)。ハッシュのidを FK にしている箇所は無い。性質の変化:
task_table_indexは「全 DB でユニーク」から**「コンテスト内でのみユニーク」**になる。unique 制約は無く、コンテスト横断で index をキーにしている箇所も無い(sortAojHeaderIdsは単一コンテスト内で閉じる)ため問題ない。3. SQL の解説
SQL の読み方
大きなクエリは内側から読み、各段階で「これは何の表を作るのか」を問う。
WITH形式ならその順序がそのまま上から下になる。lettersが作るのはtask_idとletterの 2 列だけの一時的な表。これが分かれば
UPDATE側は単純で、「taskとlettersをtask_idで突き合わせ、一致した行にletterを書き込む」だけ。PARTITION BY contest_idORDER BY task_id::introw_number()bigintなので::intが要る)chr(64 + n)chr(65)=Aなのでn=1 → A、n=2 → Bcontest_id ~ '^JAG(Summer|…)'AOJ_JAGには PR1 で済んだ模擬国内も含まれるため、接頭辞で範囲を絞るPARTITION BYが無いと対象の全問題に通し番号が振られ、2 つ目以降のコンテストがZを超えて壊れる。実データでの確認:
ICPCPrelim2024(9 問)は1672 → A,1673 → B, …1680 → I。ICPCPrelim1998は 4 問なのでA〜D。計算量 — O(N²) にはならない
WHEREの条件は join ではなくフィルタ。1 行あたり O(1) で評価され、行数を減らす方向にしか働かないtask_idに UNIQUE インデックスがある(prisma/migrations/0_init/migration.sqlのtask_task_id_key)ため、Nested Loop + Index Scan なら O(M log N)、Hash Join なら O(N + M)。二乗にはならない推測ではなく実行計画で確認すること。 PR1 では
taskの Seq Scan 2 回 + Hash Join だった(contest_typeに索引が無いため想定どおり)。4. 検討の経緯(採らなかった案)
以下はいずれも計画で却下・保留と決定済み。 判断の理由を残すための記録。PR3 の採らなかった案は plan.md にまとめた。
titleに記号を入れる案を採らない理由titleを'A. Peculiar Protocol'に揃えれば AtCoder 側 1,366 件と形式が揃うが、採らない。「入れない」を選んでも AtCoder と AOJ で title の意味は揃わないが、 どちらを選んでも不整合は残る(AtCoder 側を触るのはスコープ外)。AOJ を「素の title」に寄せ、セルで
A.を付ける方が、PR1 の設計(buildTitleWithTaskIndex)と整合する。管理画面案を保留にした理由
「管理画面から SvelteKit の action で更新する」案には実質的な利点がある。
一方で、この案は問題記号の入力元が定義できないと成立しない。手入力なら 1,000 回超の手作業に戻り、難所 1 で否定したものに戻る。コード上の対応表を読むなら、実質「対応表を DB に同期するボタン」になる。
さらに、素直に実装すると N+1 になりやすい。既存の
createTaskがまさにその形で(src/routes/(admin)/tasks/+page.server.tsのPromise.all+ check-then-act)、トランザクションが無いため途中で失敗すると部分適用が残る。採るならupdateManyか$executeRawで一括処理する設計が前提になる。再検討の結果(2026-10-24): PR2 の照合で補正が要るのは 8 コンテストと分かり、マイグレーション 1 本で足りると判断した。補正の誤りが繰り返し見つかるようなら再検討する。
5. Task テーブルへの書き込み経路の全数調査
リポジトリ全体(
src/prisma/e2e)を確認した結果。移行 UPDATE の安全性の根拠。書き込み経路は 3 つだけ、削除経路はゼロ
src/lib/services/tasks.tsのcreateTaskid/task_id/contest_id/contest_type/task_table_index/titlesrc/lib/services/tasks.tsのupdateTaskgradeのみprisma/seed.tsgrade(idは未指定)updateTaskの呼び出し元は 2 つ(src/routes/(admin)/tasks/[task_id]/+page.server.ts、src/routes/(admin)/tasks/grade/+page.server.ts)。いずれもgradeのみtask.delete/deleteMany/upsert/createManyはリポジトリ全体に存在しない$executeRaw/$queryRaw)による Task への書き込みもなしtask: { create: ... }もなし⇒ 一意フィールドを UPDATE するコードは構造的に存在しない。 リポジトリの外では、staging / production に Supabase の CSV で行を入れることがある(既存の合宿系、PR2.1)。
idの採番規則が経路ごとに違うsha256(contest_id + task.id)(16 進)— 決定的sha256(contest_id + title)(16 進)。メンテナが別プロジェクトのスクリプトで作る(2026-10-24 にメンテナが説明)id未指定のため fabbrica の自動採番で連番文字列一意性は破れないが「
Task.idはtask_idから決まるハッシュ」という前提は取り込み以外では成立しない。ただしTask.idを再計算して引く箇所はゼロで、全ルックアップはtask_id経由。現状の実害はないが、将来Task.idに決定性を期待するコードを書くと静かに壊れる。check-then-act の競合
createTaskはisExistsTask(findMany)→createの check-then-act で、src/routes/(admin)/tasks/+page.server.tsがこれをPromise.allで並列実行している。同一
task_idが 1 回の取り込み JSON に 2 件含まれると、両方がチェックを通過して 2 件目が P2002 で落ちる。Promise.all全体が reject して 500 を返すが、 トランザクションが無いため先に成功した分はロールバックされない(部分適用)。prisma/seed.tsも同じ形で PQueue concurrency 3 で並列実行しており同種のレースがある。一意性は DB 制約が守るので壊れはしないが、エラー時の挙動が中途半端。
docs/guides/agent-rules/prisma-db.mdの「Idempotent Writes」に該当し、本来はcreateMany({ skipDuplicates: true })かupsertが解。本件とは独立の既存課題(O5)。他の sha256 決定的 ID は Task から独立
src/lib/services/answers.tsとsrc/features/votes/services/vote_grade.tsはいずれも入力がTask.idではなくtask_id。vote_grade.tsは全てupsertでwhereに複合ユニークを使うため id 衝突の経路もない。削除経路がないことの帰結
Task を削除するコードが存在しないため、関連テーブルが孤児 FK を持つ事故は現状起こらない。将来 Task 削除機能を追加する場合(PR3 のダミーの扱いなど)、6 つの関連のうち
TaskTag/TaskAnswer/WorkBookTaskはonDelete未指定なので要注意(投票系 3 つはCascade)。6. 調査中の誤りと訂正(自戒)
対象を 632 問と集計した — Provider が存在する 3 種のみを数え、seed にデータがある合宿系 467 問を落としていた。正しくは 1,099 問。 教訓: 「実装がある範囲」と「データがある範囲」は一致しない。集計条件は後者で取る。
「問題 ID が連番だから欠落なし、機械生成できる」と結論した — 欠落問題は ID も採番されないため、連番性は欠落の有無について何も語らない。検証が問いに答えていなかった。 教訓: 「観測できる範囲に異常が無い」ことを「異常が無い」と読み替えない。欠落は定義上、観測できない側にある。
抽出スクリプトのバグを、データの欠落として提示しかけた —
prisma/tasks.tsからタスクを抜き出す正規表現がシングルクォート前提だったため、name: "A Bug That's Not a Pill Bug"(アポストロフィを含むためダブルクォート)を取りこぼし、ICPCPrelim2024が 8 問に見えた。実際は 9 問で、欠番は存在しなかった。 教訓: 抽出スクリプトを書いたら、まず総数が既知の値と一致するかを確認する。1 と同じ「観測範囲を実際の範囲と取り違える」失敗であり、本件では欠落の誤検出に直結する。移行 SQL を seed 由来の対応表から生成する案を出した — production のタスクは管理画面の取り込み経由で入っており seed と一致する保証がないため、production にしか無い行が取り残される。しかも
UPDATEは該当行が無くてもエラーにならない。 教訓: 2 と同じ型。「seed で見えている範囲」を「production の実態」と取り違えた。AtCoder 系の復元を「不可能」と断定した — 実際は
task_idのサフィックスから 83.4% が導出できる。ただし事故対応としては成立しないという結論自体は変わらない。 教訓: 断定する前に測る。結論が同じでも、根拠が誤っていると次の判断で誤用される。PR を分けた後も、移行 SQL の範囲を 1,099 件のままにしていた(2026-10-21 に発見)—
contest_type IN ('AOJ_ICPC', 'AOJ_JAG')は合宿系も含むため、PR1 で 467 件を余計に更新するところだった。 教訓: 範囲を分割したら、範囲を表す条件をすべて追従させる。分類(contest_type)と分割の単位が一致しているとは限らない。欠落した問題を「AOJ に存在しない(ID も無い)」と決めつけていた(2026-10-21 に発見)— ICPC 国内予選 2002 の E は AOJ に問題 1123 として存在し、コンテスト一覧(取り込み元)から外れているだけだった。PR3 でダミーを作るという前提は、この問題には当てはまらない。 教訓: 「一覧に無い」と「存在しない」は別。データの取得元の範囲を、対象が実在する範囲と取り違えない(1・4 と同じ型)。
7. PR3: 欠けている問題
plan.md の PR3 の節 の根拠。確認はすべて 2026-10-28 時点(メンテナのスクリーンショットと AOJ の API、リポジトリのコード)。採らなかった案は plan.md に 1 か所にまとめた。
調べ方と結果
公式の問題なのに seed に無いものを、AOJ の問題一覧 API で問題名から探した(部分一致)。対象は、スプレッドシートで「AOJ 未収録」とある 22 問、スプレッドシートのリンク先の title が AOJ で見つからなかった 3 問(JAG 模擬地区 2013 の A〜C)、PR1 で見つかった 1 問(ICPC 国内予選 2002 の E)。
結果は 26 問で、AOJ にあるものが 5 問、AOJ に無いものが 21 問。
AOJ にある 5 問
どれも AOJ のコンテスト一覧には無いが、問題一覧 API に同じ問題名で載っている。
availableが 4(ほかの問題は 1)で、提出が 0。公開前とみられるが、availableの値の意味は未確認AOJ に無い 21 問
表記ゆれのありそうな語(
Wireing、Shortest、Geometry)も部分一致で探したが、見つからなかった。Wireing Assistantは原文どおりの綴り。The "Shortest" Pair of Pathsの引用符は半角jag-icpc.org
JAG のサイトで、スプレッドシートの情報源の一つ。URL の形式(
?plugin=attach&refer=...&openfile=...)から PukiWiki とみられる。年度/Practice/大会区分/問題文とデータセット/(その下に問題文の PDF と注釈)。問題文とデータセットと同じ階層に、講評などのページもあるday1.pdfなど日ごと、2008 年模擬地区予選はproblems.pdf)Flame of Nucleusの問題を、一覧はFrame of Nucleusと書いている)。表記が食い違ったら、問題文の PDF の表紙で判断する記号がずれないか
PR3 で記号(
task_table_index)がずれる心配は、事実上ない。seed_problem_letters.test.tsが記号を 1 問ずつ確かめる(plan.md の PR3 の節)残るのは人の作業の誤りで、どちらも確かめる手順がある:
task_idを UPDATE する(手順)ダミーで足す記号:
AOJ にある 5 問を足すと、ICPC 国内予選 2002 は A〜E、模擬地区 2013 は A〜J、夏合宿 2024 Day3 は A〜K になる。
ダミーの
task_idNoJudge-{contest_id}_{記号の小文字}(例:NoJudge-JAGWinter2006-day2_a)。task_table_indexは大文字(A)のまま。startsWith('NoJudge-')で足りるabc300_aなど)とも構造上ぶつからないabc300_a、jag2018summer_day2_a)に合わせるためcontest_idと記号の組が一意なので、ID も一意になる-・_だけなので、/problems/{task_id}にそのまま入るリンク先
jag-icpc.org の大会区分のページ(例:
2008/Practice/模擬地区予選)にする。リンクの出し方
getTaskUrl()(src/lib/utils/task.ts)の URL ジェネレータの並びにNoJudgeGeneratorを足す。問題へのリンクはすべてgetTaskUrl()を通るので、1 か所で全画面に効く。canHandleが真になったものを使う)に沿う。getTaskUrl()の本体は変わらず、task.tsの TODO(Codeforces・yukicoder・BOJ)も同じ形で足せるUrlGenerator.canHandle(src/lib/types/url.ts)にtaskIdを足す。使うのはtask.tsだけで、既存の 2 つの実装は引数が少ないままでも型を満たすcontest_idを使うので、NoJudgeGeneratorをAojGeneratorより前に置かないと、AOJ 側が先に受け取る。コメントとテストで固定するisNoJudgeTask()。ダミーかどうかの基準が 1 つになる{ url, taskIds }をページごとに並べて組み立て、各 URL を 1 回だけ書く出典のラベル
ダミーの出典は
(AOJ)ではなく(No Judge)にする(例:JAG 冬合宿 2006 Day2 - A(No Judge))。(AOJ)と出すのは誤りtask_idの接頭辞NoJudge-と言葉がそろうaddContestNameToTaskIndex()はcontest_idと記号しか受け取らない。ダミーは本物の AOJ の問題と同じcontest_idを使うので見分けられず、taskIdを足すtaskIdは省略できない引数にする。型チェックが呼び出し元 6 か所の漏れをすべて見つける。どの呼び出し元もtask_idを持っているアイコンもツールチップも入れない理由
採点が無いことは、出典のラベルの
(No Judge)で文字として伝える。コンテスト別の表のセルには出典のラベルが出ないので、表の見た目は変わらない。ExternalLinkWrapperのiconSize={0})影響範囲
変更が要る機能は、問題へのリンク(
getTaskUrl())と出典のラベル(addContestNameToTaskIndex())の 2 つ。ほかは Task のレコードを足せば動く。contest_idで絞り、task_table_indexの順に列を並べて置く(sortAojHeaderIds、generateTable)。task_idは使わないaoj_jag_regional_entries.ts)があるtask_table_indexを付けて「A. Monkey Business」と出す。title の先頭の「記号. 」を外してから付けるが、21 問の title はどれも「記号. 」で始まらないgetTaskUrl()を通る(呼び出し元 9 ファイル)NoJudgeGeneratorを足すaddContestNameToTaskIndex(contestId, taskTableIndex)がcontest_idだけを見て、AOJ 系なら(AOJ)を付ける。呼び出し元は 6 ファイル(各 1 か所)taskIdを足し、ダミーは(No Judge)にするtask_idへの外部キー(6 テーブル)/problems/{task_id}compareByContestIdAndTaskId()がコンテストの優先度 →contest_id→ 記号の順に並べ直す。DB から取る順(task_idの降順)は効かないtask_idが空でないことだけを見るassignProblemLetters()のNumber()だけ。管理画面の AOJ 取り込みと、seed_problem_letters.test.tsの基本ルールの検査で使う。ほかに PR1 / PR2 のマイグレーション(20261024033928、20261025000719)がORDER BY task_id::intで並べるLETTERS_OF_SHIFTED_CONTESTSに入れて検査から外す。マイグレーションは本番・staging とも適用済みで、新しい DB では seed より先に流れるので、ダミーの行を読むことはないtask_idを_で分ける箇所getCourseName()だけ。AOJ の API の応答にしか使わないレコードの入れ方:
prisma/tasks.tsに足す。contest_typeはclassifyContest()がcontest_idから決める(ダミーの 7 コンテストはAOJ_JAG)AOJ に後から追加されたとき
3643 のように、AOJ は過去の問題を後から追加することがある。ダミーの問題が AOJ に入ったら、AOJ の ID に移す。
assignProblemLetters())。すでにある行は飛ばすので今の記号は変わらないが、欠けたコンテストに後から足された問題には、ずれた記号が付く(例: 模擬地区 2005 の C が新しい ID で入ると、7 番目の G になる)。同じコンテストで記号が重なると、generateTableが片方を上書きし、問題が黙って表から消える((contest_id, task_table_index)に一意制約は無い)。この危険は PR3 の前からあり、対象は管理画面から取り込める模擬地区(ダミーがあるのは 2005 / 2008)。合宿系は AOJ の一覧に無いので取り込めないtask_idを AOJ の ID に UPDATE する。参照する外部キーは 6 つともON UPDATE CASCADE(taskanswer / tasktag / workbooktask / votegrade / votedgradecounter / votedgradestatistics)なので、回答・投票・タグ・問題集のデータは付いてくる。記号もそのまま残るno_judge.tsの map から消し、seed の行のidとseed_problem_letters.test.tsの期待値を直すtask_idから決まるので、自動で(AOJ)に戻る移し忘れても、
NoJudge-が残っていれば一目で分かる。Merge pull request #4245 from AtCoder-NoviSteps/#4223