検索
ニュース

「100万行コード移行は4年がかり」はもう昔話 Claude Codeで「2週間」に縮める6ステップ/ベストプラクティスJavaScriptランタイム「Bun」のコードベースをZigからRustに移行(2/3 ページ)

Anthropicは、本番コードベースを別の言語に移植する大規模なコード移行プロジェクトをどのように実施しているかを解説したブログ記事を公開した。

Share
Tweet
LINE
Hatena

ステップ1:ルールブック、依存関係マップ、ギャップインベントリを作成する


ステップ1の概要(提供:Anthropic)

 ルールブックは、コードの翻訳方法を定義する。その具体的な形は、「新しいコードが元の構造を維持するかどうか」(サムナー氏のプロジェクト)、「新しいコードを完全に再設計するかどうか」(クリーガー氏のプロジェクト)などで決まる。前者では、ルールブックは、型やイディオムを対応付けるルックアップテーブルが中心となり、後者では設計文書となる。

 サムナー氏は、Claudeと対話しながらルールブックを構築し、よくある失敗モードの8つのカテゴリーを検査する8つのサブエージェントにレビューさせた。

 依存関係マップは、並列ワークストリームの順序付けと関連ファイルのバッチ化に使う。明示的なマニフェストを持つエコシステムもあるが、「C」「C++」、Pythonのような言語では、Claude Codeがエージェントを展開し、依存関係を発見する決定論的スクリプトを作成、実行する。

 ギャップインベントリは、ルールブックのデフォルト(既定)では扱えない暗黙知を捕捉する。ZigからRustへの移行では、手動メモリ管理が、PythonからTypeScriptへの移行では、Pythonが宣言を強制しないインタフェースと契約がこれに当たる。

 Zigでは、メモリ解放を忘れた呼び出し側もコンパイルを通過し、リークは実行時に初めて表面化する。

fn readConfig(allocator: std.mem.Allocator) ![]u8 {
    const buf = try allocator.alloc(u8, 1024);
    // ...fill buf...
    return buf; // caller must free this -・but only the comment says so
}
// A caller that forgets 'defer allocator.free(buf)' still compiles -・the leak only surfaces at runtime.

 Rustでは、所有権が呼び出し側に移動し、メモリは自動的に解放される。

fn read_config() -> Vec<u8> { 
let buf = vec![0u8; 1024]; 
// ...fill buf... 
buf // ownership moves to the caller; memory is freed automatically 
} 
// Use it after it's moved? Free it twice? Neither compiles. 
// Forget to free it? There's no free call to forget -・drop is automatic.

 Pythonでは、どのオブジェクトが渡されるかは、コードベース全体を読まないと分からない。

def register(handler):
    handler.setup()
    return handler.run({"retries": 3})
# Any object with .setup() and .run() works here. Which objects actually get passed in? Read the whole codebase to find out.

 TypeScriptでは、契約を明示的に記述しなければ、コンパイルを通過しない。

interface RunResult { ok: boolean } 
interface Handler 
{ setup(): void; 
run(opts: { retries: number }): Promise<RunResult>; 
} 
function register(handler: Handler): Promise<RunResult> { 
handler.setup(); 
return handler.run({ retries: 3 }); } 
// The contract must be written down before this compiles

ステップ2:ルールをストレステストする

 このステップは、大規模な移行に向けた“試運転”となる小規模な移行だ。


ステップ2の概要(提供:Anthropic)

 サムナー氏は、エージェントの一つにルールブックを使って3つのファイルを翻訳させ、別のエージェントに「シニアRustエンジニアのように」同じファイルを翻訳させ、さらに別のエージェントに、両者の差分を用いて新しい翻訳ルールを作らせた。

 これにより、全1448ファイルに拡散する前に、2つの重大な問題を発見できた。

 この種のストレステストは、「構造を維持する移行」でのみ有効だ。クリーガー氏のプロジェクトのように、コードを完全に再設計する場合は、同等なテストとしては、敵対的レビュアーを通じて設計文書を直接検証し、その後、使い捨てのエンドツーエンド実行で妥当性を確認することになる。

 いずれの場合も、翻訳済みファイルは破棄する。このステップの目的はルールを洗練させることであり、作業を進めることではない。

ステップ3:全てを翻訳する


ステップ3の概要(提供:Anthropic)

 以降のステップでは、「実装、レビュー、修正」というマルチエージェントループを実行する。実装は、小規模なモデルに任せられる。クリーガー氏は、Claude Sonnetで12のサブエージェントを展開した。レビュアーには、より大規模なモデルが割り当てられる。

 作業キューは、機械的に管理する。バッチスクリプトが「翻訳済みファイルがディスク上に存在するかどうか」で完了を判定するので、移行は構造的に再開可能になる。エージェントが自信を持って翻訳できない箇所は、「TODO(port)」コメントを付けて後のステップに回す。

 2つの敵対的レビュワーが別々のコンテキストで成果物を評価し、意見が割れた場合は3つ目のエージェントが裁定する。レビュワーが同じミスを繰り返し検出したら、修正はファイル単位では行わない。ルールブックに1文を追加し、影響を受けたバッチを再生成する。

 重要な設計判断の一つが「コンパイラをどこに置くか」だ。クリーガー氏は、ユニットを数秒でチェックできるTypeScriptコンパイラを全ループ内で実行した。サムナー氏は、コンパイラをループから排除し、コンパイルを次のステップに先送りした。cargoに数分かかるからだ。

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る