10月16日、thecloudlet.github.ioが「Refactoring with AI, Part 1: Deleting 19,000 Lines Without Trusting It」と題した記事を公開した。AIを活用しながらも盲目的には信頼せず、約1万9000行のコードを1週間で安全に削除し、1ヶ月後もrevertゼロを達成したリファクタリングの実践手法を解説している。
「触るな」という圧力に抗う
「動いているなら触るな」——多くのエンジニアが一度は聞いたことがある言葉だ。記事の著者もかつての職場でスタッフエンジニアからそう助言された。理由は二つ:クリーンアップは「仕事として認識されない」こと、そして「最終的にrevertで終わる」こと。
著者はこれに反論する。ソフトウェアは熱力学の第二法則に従い、放置すれば無秩序に向かう。現に著者は、副作用だらけのメンバ関数に埋め込まれたlist1とlist2という名の静的リストが何をするのか解読するだけで4日間を費やした経験がある(この具体例は元記事「Why Bother?」節で挙げられているエピソードをそのまま引用している)。
加えて、AIの登場がデッドコード放置のコストを引き上げた。モデルはコンテキストウィンドウ内のコードを「有効なパターン」として読むため、死んだコードが新たなコードに悪影響を与える。一方、AIは依存関係の追跡という従来最もコストの高かった作業を得意とする。これが今回の大規模削除を可能にした背景だ。
課題:本番コードに絡みついたフラグ
削除対象は、コンパイラオプション--XYZで制御される「フローX」と呼ばれる旧来の処理フローだった。後継フローへの移行は完了していたが、フローXのコードは本番稼働中のコードと絡み合った状態で残っていた。
目標は明確に二つ:
- 完全削除:フローXおよび
--XYZに関するコード・テスト・ドキュメント・スクリプトを残さない - 振る舞いの変化なし:生き残るフローの出力が削除前後で一致すること
核心:「AIを信頼しない」ための5ステップ
著者が強調するのは「AIがほとんどの作業をした、そして私はAIを一切信頼しなかった。この二つは矛盾しない」という姿勢だ。その具体的な方法論が5段階のプロセスとして整理されている。
Step 1:スコープの特定(どこを読むべきか)
まず--XYZがパースされる地点を起点に、compile_commands.jsonベースの静的解析ツールで呼び出しグラフを追跡。LLMは到達した関数にコメントを付与する:
- RED:フラグの値によって振る舞いが変わる
- BLUE:フラグのデータを転送・準備する
// BLUE: forwards the flag's value into the pipeline config.
PipelineConfig buildConfig(const Options &opts) { ... }
// RED: the starting point. Behavior depends on the flag.
void buildPipeline(PassManager &pm, const PipelineConfig &cfg) {
if (cfg.useFlowX)
addFlowXPasses(pm);
else
addDefaultPasses(pm);
}
// RED: reached only through the flag's branch.
void addFlowXPasses(PassManager &pm);
マーキング結果は別の呼び出しグラフツールで検証する。AIの出力をAIで検証するのではなく、既存の静的解析ツールでクロスチェックする点が肝だ。
Step 2:ノードの分類
スコープが定まれば、次は各ノードをリスクに応じて4分類する:
| カテゴリ | 内容 |
|---|---|
| 機械的削除 | フラグ除去後に到達不能。丸ごと削除 |
| クロスオーナーシップ | 他チームが所有するコードと絡んでいる |
| 副作用 | 共有状態を変化させる。手動対処 |
| 外科的処置 | 生きているコードと絡んでいる。切除して縫合 |
LLMが分類を誤ったケースもあったが、全て「より安全なカテゴリへ格下げ」で対処できた。典型的なミスは、C++コードからは呼ばれていないが文字列でパスを登録しているファイルを「機械的削除」と誤分類したケースだ。この分類ステップこそが、著者が実際に目を通した「どの12%を読むか」を決める作業でもある。
Step 3:足場テストの設置
削除の前にテストを書く。著者は二種類のテストを用意している。一つは削除テストで、削除前に失敗し、削除後に通ることで「確かに消えた」を証明する。もう一つは外科的機能テストで、生きているコードへの切除・縫合の前後で振る舞いが一致するかを確認する。
これらのテストは足場(スキャフォールディング)であり、作業完了後に削除する。内部実装の詳細を検証するテストを長期間残しても保守コストが増すだけだからだ。先にテストという検証可能な構造を置くことで、AIの作業結果を「信頼」ではなく「証拠」で評価できる状態を作る。これがStep 1の静的解析によるクロスチェックと並ぶ、著者の方法論の核だ。
Step 4:削除と外科的処置
AIがカテゴリ別に編集を実施する。サブエージェントが最初にレビューし、著者が最終確認する二段構えだ。このフェーズではコード品質の改善はスコープ外とし、振る舞いの正確性だけに集中する。各コミットを1カテゴリに対応させることで、レビューの粒度が一定に保たれ、問題が起きた際の差し戻し範囲も最小化できる。AIへの作業委譲範囲をカテゴリ単位で明示的に区切っている点も、「信頼しない」姿勢の実践と言える。
Step 5:術後クリーンアップ
削除後、コードはかつての複数モードのために設計されたままの形を残している。std::variantのケースが一つになった型、一つのルートしかないルーティングロジックなどを整理する。このステップはStep 4とは明確に分離されており、正確性の確認が終わった後にはじめて可読性・簡潔さを追求する。
Before(Step 4後):
enum class PipelineMode { Default };
struct PipelineConfig { PipelineMode mode = PipelineMode::Default; };
std::expected<void, std::string>
buildPipeline(PassManager &pm, const PipelineConfig &cfg) {
switch (cfg.mode) {
case PipelineMode::Default:
addDefaultPasses(pm);
return {};
}
return std::unexpected("unknown pipeline mode"); // 到達不能
}
After(Step 5後):
void buildPipeline(PassManager &pm) { addDefaultPasses(pm); }
結果:1週間で1万9000行、1ヶ月後もrevertゼロ
| 指標 | 値 |
|---|---|
| コンパイラバイナリサイズ | −50MB超 |
| 削除/追加コード行数 | 約19,000 / 約130 |
| 削除テストファイル数 | 約30ファイル(約17,000行) |
| 修正ファイル数 | 約35ファイル(約2,300行) |
| revertまたは修正コミット | 0 |
著者が実際に行を読んで精査したのは、削除したコードの約12%のみだった。どの12%を読むかを決めたのが、ステップ2の分類作業だ。
コミット履歴も整然としており、各コミットは1カテゴリに対応し、1回のレビューセッションで確認できる規模に収まっている。
要点
著者の姿勢を一言で言えば「AIに仕事をさせ、検証は自分でやる」だ。LLMの出力を逐一再実行するのではなく、色付け・分類・テストという検証可能な構造を先に作り、そこにAIを当てはめることで、信頼ではなく検証を根拠に大規模変更を成立させた。
詳細はRefactoring with AI, Part 1: Deleting 19,000 Lines Without Trusting Itを参照していただきたい。