Skip to content

필사 모드: AIでコードベースを移す実践手順 — 審判を先に立て、行数ではなくレビュー率を測る

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに — 順序を間違えると、規模が大きくなるほど損失が大きくなります

2026年上半期に大規模LLMマイグレーションの事例が立て続けに公開されました。Bunは53万5千行のZigを11日でRustへ移し(発表文)、AnthropicのMike Kriegerは16万5千行のPythonを週末ひとつでTypeScriptへ移しました(方法論のまとめに載っている事例)。二つの事例は言語も規模もチーム構成も違いますが、手順は驚くほど同じです。

そしてその手順でもっとも重要なのは、個々の技法ではなく順序です。翻訳を先に始めて検証をあとから付け足すと、規模が大きくなるほど損失が指数的に膨らみます。100ファイルを間違って移してからルールが誤っていたと気づくなら100ファイルを回し直せば済みますが、1,400ファイルを移したあとで気づいたなら、その時点ではすでに次の工程の作業がその上に積み上がっています。

この記事は、公開された一次ソース(Anthropicのマイグレーションキットと方法論ドキュメント、Bunの発表文)から再現可能な手順だけを抜き出して順に並べたものです。特定のモデルやツールに縛られない部分だけを残し、検証されていない箇所はそうだと書きました。Bunの事例そのものの分析は別の記事で扱ったので、ここでは手順だけを見ます。

ステップ0 — 翻訳より先に審判を立て、審判を先にデバッグする

もっともよくある間違いは、この工程を飛ばすことです。マイグレーションは人のレビューを機械の判定に置き換えることで速度を得ます。判定者がいなければ、速度を得たのではなく検証を省いただけです。

振る舞いのオラクルは、三つのうち少なくとも一つで作ります。

移植可能なテストスイートがもっとも良い選択です。条件は一つ — テストが公開表面だけを触っていることです。内部関数を直接呼んだり、プライベートな状態を検査したりするテストは言語が変われば一緒に捨てることになるので、オラクルにはなれません。Bunの11日を作った決定的な条件がここにありました。テストがTypeScriptで書かれていて、ランタイムの実装言語とは無関係であり、移植の過程で削除されたりスキップされたりしたテストは0件でした。

ゴールデン出力は、テストが手薄なレガシーでの第二の選択肢です。実際の入力の束を決めて、旧実装の出力をファイルに固定しておき、新実装の出力とバイト単位で比較します。バッチジョブ、レポート生成器、パーサーのように入出力が決定的なシステムでよく機能します。

差分実行は三つの中でもっとも強力です。同じ入力を二つの実装へ同時に流し、結果を比較します。Kriegerの事例がこの形で、実シナリオ7件でパリティハーネスを作り、どんな振る舞いの変化もバグとみなすというルールを立てました。「意図した改善」という例外を許さないことが核心です。例外を許した瞬間に、オラクルは交渉の対象になります。

そしてここに、ほとんどの人が見落とす工程がもう一つあります — 審判を先にデバッグします。オラクルを元のコードに走らせて全部通るか、そしてわざと壊した元コードに走らせてきちんと落ちるかを確認しなければなりません。全部落とす審判は、たいてい正しいのではなく壊れています。

# 審判の検証: 通るべきときに通り、落ちるべきときに落ちるか
git stash list >/dev/null 2>&1 || exit 1

# 1) 無傷の元コードでは全部グリーンでなければならない
./run-oracle.sh --target=legacy || echo "FAIL: 審判が元コードすら通せない"

# 2) わざと壊した元コードでは必ず赤が出なければならない
#    (比較演算を一つ反転させる程度の最小変異で十分だ)
git apply mutations/flip-one-comparison.patch
./run-oracle.sh --target=legacy && echo "FAIL: 審判が変異を捕まえられない。まずオラクルを直すこと"
git apply -R mutations/flip-one-comparison.patch

この二行を走らせるのにかかる時間は、あとで「テストは全部通るのに本番が壊れる」をデバッグする時間より、いつでも安く済みます。

ステップ1 — ルールブックと依存関係マップ、そして3ファイルの予行演習

ルールブックは、翻訳の判断を一度だけ下すための文書です。キットが付けているメタルールが判別基準としてそのまま使えます — 二つのエージェントが違う答えを出しうる問いなら、それはルールブックの項目です。エラー処理のイディオムの対応付け、nullの表現、整数オーバーフローの方針、ログのフォーマット、命名規則、並行性プリミティブのマッピングが典型的な項目です。BunのPORTING.mdは約600行で、書くのにかかった時間はコーディング前の約3時間でした。

依存関係マップは決定的なスクリプトで作ります。モデルに「このファイルは何に依存していますか」と尋ねてはいけません。import文をパースする30行のスクリプトのほうが正確で再現可能です。このマップの成果物が作業順序(葉から根へ)と循環の一覧です。循環は前もって知っておく必要があります — 元の言語が寛容に見逃していた循環依存が、対象言語で数千個のモジュールエラーとして一度に噴き出すのは、この作業の定番の事故です。

ギャップインベントリは、対象言語が新たに要求する情報の一覧です。ZigからRustへ行くときは所有権とライフタイム、PythonからTypeScriptへ行くときは明示的なインターフェース契約がここに入ります。元のコードに存在しない情報なので、翻訳ではなく決定です。決定は人が下し、ルールブックに書きます。

その次が予行演習です。ファイルを3つだけ選んでパイプライン全体を回します。あるエージェントはルールブックに従って翻訳し、別のエージェントはルールブックなしでその言語のシニアのように翻訳し、三つ目が二つの結果の差を監査します。ここで出てきた翻訳物は全部捨てて、ルールだけを直します。Bunはこの段階で二件を捕まえましたが、そのまま進んでいたら1,448ファイル全体に広がっていた問題でした。人の時間を注ぎ込む地点があるとすれば、ここです。

ステップ2 — 検証可能な境界で切り、キューをディスクに任せる

作業単位を選ぶときの基準は大きさではなく検証可能性です。単位を一つ移したあとで「できたかどうか」を機械が判定できなければなりません。ファイル単位がよく合うのは、対象ファイルの存在の有無とコンパイル成功の可否で判定が終わるからです。

ここで構造保存と再設計を分けなければなりません。ファイル対ファイルの対応を保つ構造保存の移植なら、予行演習も行単位の突き合わせもファイル単位のキューもすべて成立します。一方、移しながらモジュール境界を引き直すと決めた瞬間、これらの装置が同時に崩れます — 突き合わせるべき元の行がなくなり、単位がモジュールにまで膨らみ、巻き戻しの半径も一緒に大きくなります。移すことと引き直すことを一度にやろうとする誘惑が、この手順のもっともよくある失敗原因です。順序を分けてください。まず移し、オラクルがグリーンの状態で引き直します。

完了判定はディスクの状態に任せます。この一つの決定が、再開可能性と巻き戻し可能性を同時に与えてくれます。

# キュー = マニフェストのうち、まだ出力ファイルがない項目。それ以上の状態は持たない。
awk -F'\t' '{ if (system("[ -f " $2 " ]") != 0) print }' migration/manifest.tsv \
  > migration/queue.tsv

wc -l < migration/queue.tsv        # 残りの作業量
git worktree list                  # ワークツリーごとに別のキュースライスを与える

進捗をエージェントに尋ねた瞬間、進捗そのものがモデルの出力になり、幻覚の対象になります。ファイルの存在の有無は幻覚しません。

巻き戻し可能性は三つで保ちます。単位ごとにコミットを分けて巻き戻しの半径をファイル一つに保ち、翻訳の工程と修正の工程を別のブランチかワークツリーに分け、ルールブックの改訂時点をコミットメッセージに残します。最後の項目が重要なのは、ルールが変わったときに、それ以前に作られた成果物のうち再生成すべき範囲を特定できなければならないからです。

コンパイラをループのどこに置くかも、この段階で決めます。TypeScriptやGoのように型チェックが速いなら翻訳ループの中に入れて即座にフィードバックを受け、Cargoのように遅いなら翻訳がすべて終わったあとに一度回し、エラー一覧を次のキューとして使います。Bunは後者で、約1,600個のコンパイルエラーを12時間で片づけました。

ステップ3 — 何を測るか: 変換した行数は進捗ではない

マイグレーションのレポートでもっともよく見かけ、もっとも役に立たない数字が「変換した行数」です。この数字は出力量であって進捗ではありません。実際に意思決定を変える指標は次の五つです。

指標定義何を教えてくれるか
審判通過率オラクルのケースのうち新実装が通した割合唯一の完了の定義。Bunは972の失敗ファイルから23、さらに0まで下げた
ルール違反再発率レビュー指摘のうち、すでにルールブックにあった項目の再違反の割合値が下がらないなら、問題はルールではなくプロセスだ
差分レビュー率人が実際に読んだ行数 割る 生成された行数リスクの実際の大きさ。100万行ならこの値は正直に一桁パーセントだ
単位あたり手戻り回数ファイル一つがキューに戻された平均回数2回を超えたら、ルールブックがその領域をカバーできていない
残存マーカー数TODO(port)、BUG(port) コメントの残り個数マージ後にも残る実際の負債。マージ日を完了日と取り違えないようにしてくれる

このうち差分レビュー率は必ず計算して記録することを勧めます。100万行規模でこの値を正直に計算すると、たいてい5パーセントを下回ります。その数字が居心地悪いなら、その居心地の悪さが正確な反応です。それがこのやり方が引き受けているリスクの大きさであり、意思決定者に報告すべき値です。隠さずに、代わりに残りの95パーセントを何が判定したのか(コンパイラ、オラクル、敵対的レビュアー)を併せて書いてください。

レビューの経済学はこう整理されます。人が読むべきなのは生成されたコードではなく、生成のルールと、そのルールが作った代表サンプルです。ルールブック600行、予行演習3ファイルの差分、そしてレビュアーたちが異議を出したケース。この三つは人が全部読めますし、読めば実際に結果が変わります。逆に1,448ファイルをざっと眺めるレビューは、時間を使っても結果を変えません。

ステップ4 — 三つの失敗モードとそれぞれの検出法

静かな意味のずれ。コンパイルも通りテストも通るのに、振る舞いが微妙に違う場合です。浮動小数点の丸め、ソートの安定性、nullと空値の区別、エラー伝播の順序、タイムゾーンの扱い、整数オーバーフローの挙動あたりでよく出ます。テストがカバーしていない経路で起きるので、テストでは捕まりません。検出法は差分実行ひとつだけです — 本番トラフィックのサンプルや実際の入力ログを二つの実装に流し、出力を比較してください。Kriegerが「どんな振る舞いの変化もバグ」というルールを立てた理由がこれです。例外リストを作り始めた瞬間に、この失敗モードを止める手段が消えます。

幻覚API。存在しない関数、誤ったシグネチャ、古いバージョンの引数順を呼び出すコードです。静的型付け言語へ移す場合はコンパイラが全部捕まえてくれるので、実務上は大きな問題になりません。危ないのは対象が動的言語のときです。PythonやRubyへ移すと誤った呼び出しがランタイムまで生き残り、その経路にテストがなければ本番で初めて火を噴きます。対象が動的言語なら、import解決とシグネチャ検査を別の静的解析工程として必ず追加し、その工程を翻訳ループの中に入れてください。

見当違いの理由で通ってしまうテスト。三つの中でもっとも危険です。代表的な形が三つあります。第一に、新しいコードを書いたのと同じエージェントがテストも直した場合 — 審判と被告を兼ねることになります。オラクルのファイルはマイグレーションブランチでは書き込み禁止にし、変更が必要なら人の承認工程を通すようにしてください。第二に、例外を飲み込むテスト — 失敗経路が静かに通過へと変わります。第三に、早期リターンで検証部を飛ばしてしまうテストです。

# オラクルがマイグレーション中に変異していないかを毎回確認する
git diff --stat migration-base..HEAD -- tests/ | tail -1

# 通過数ではなく「実際に実行されたassertion数」も併せて見る。
# 通過数はそのままなのにassertion数が減っていたら、テストが静かに空になったという意味だ。
./run-oracle.sh --report=assertions | tee migration/assertions-$(date +%s).txt

三つの失敗モードに共通する対応原則が一つあります。レビュアーが同じ指摘を三度したら、それは個別のバグではなく系統的な誤りなので、ファイルを直すのではなくルールブックを直して影響を受けたバッチを再生成してください。個別のファイルを手で埋め始めると、ルールとコードが乖離し、その時点から再生成が不可能になります。

おわりに — この手順の値打ちは速度ではなく、元に戻せることにあります

整理すると、順序はこうです。

  1. 振る舞いのオラクルを作り、わざと壊したコードでオラクルが落ちるかをまず確認します。
  2. ルールブックと依存関係マップ、ギャップインベントリを作り、3ファイルの予行演習でルールを叩きます。翻訳物は捨て、ルールだけを残します。
  3. 検証可能な境界で切ってキューを作り、完了判定をディスクの状態に任せて、中断と再開を無料にします。
  4. 変換した行数の代わりに、審判通過率、ルール違反再発率、差分レビュー率、単位あたり手戻り回数、残存マーカー数を見ます。
  5. レビュアーの繰り返される指摘はファイルではなくルールに反映し、影響範囲を再生成します。

この手順が与えてくれる本当の利益は速度ではありません。全部捨ててもう一度回せる、ということです。ルールブックとキューとオラクルが分離されていれば、最悪の場合でもブランチを消してルールを直し、再実行すれば済みます。何年もかかるマイグレーションがキャリアを賭ける類のプロジェクトだった理由は、元に戻せなかったからであって、難しかったからではありません。

一つだけ残すとすれば、これです — 翻訳を始める前に何が終わりを判定するのかを決められていないなら、そのマイグレーションはまだ始める準備ができていません。

参考資料

현재 단락 (1/59)

2026年上半期に大規模LLMマイグレーションの事例が立て続けに公開されました。Bunは53万5千行のZigを11日でRustへ移し([発表文](https://bun.com/blog/bun-in...

작성 글자: 0원문 글자: 7,352작성 단락: 0/59