既存コードの改修・テスト整備・大規模リファクタリングをAIエージェントに任せる【2026】レガシーと戦う実践手順
新規開発ならAIエージェントは驚くほど速い。ところが、10年動いてきた社内システムに同じノリで「ここ直して」と頼むと、直したつもりで別の場所が壊れ、テストは“通るように”書き換えられ、差分は関係ないファイルまで広がる——という事故が起きます。レガシー改修でAIを戦力にする鍵は、モデルの賢さではなく進め方(ループ設計)とガードレールです。この記事では、Claude Codeを例に「読ませる → 振る舞いを固定する → 小さく直す → 機械で確かめる → コミット」の実践手順を、判断基準とトレードオフ込みで解説します。
- レガシー改修は「いきなり直させない」。最初の成果物はコードではなく、構成説明・依存関係・危険箇所のドキュメントと CLAUDE.md
- リファクタの前に特性テスト(characterization test)で“今の振る舞い”を固定する。正しいかどうかではなく「変わっていないか」を検知するためのテスト
- ループは「小さく直す → テスト → コミット」。1コミット=1意図、差分は人が読める量に。Plan Modeで計画を先に出させる
- AIのやりがちな3大事故=テストを弱めて通す/関係ない箇所の書き換え/過剰な抽象化。hooksと差分レビューの観点で機械的・人的に止める
- 大規模移行(jQuery→React、JS→TS、Python2→3型の移行)は「共存させながら端から置き換える」分割戦略で。サブエージェントとgit worktreeで文脈と作業場所を分ける
なぜレガシー改修でAIは事故るのか
コーディングエージェントは「与えられた文脈の中で、もっともらしく目的を達成する」のが得意です。新規開発では文脈=あなたの指示なので、指示が良ければ結果も良い。しかしレガシーコードでは、本当の仕様がコードの中(と、辞めた人の頭の中)にしかありません。AIはそれを知らないまま「きれいに」直そうとします。
- 暗黙の仕様を消す:「なぜか端数を切り捨てている」「なぜか空文字とNULLを区別している」処理を、AIは“バグっぽいので直す”。実はそこに取引先との約束が埋まっている
- テストがない/あっても信用できない:検証手段がないと、AIは「動きそう」で完了を宣言する。テストが落ちると、コードではなくテストを直して辻褄を合わせることがある
- 影響範囲が読み切れない:グローバル変数、動的import、文字列で組み立てるSQL、テンプレートから呼ばれる関数……静的に追えない依存が多いほど、変更の波及を見落とす
- 文脈が溢れる:大きなリポジトリを1つの会話で全部読ませると、前半で読んだ重要な制約が後半で薄まる
つまり問題は「AIが下手」ではなく、検証の仕組みがないまま、広い範囲を一度に変えさせていることです。人間のチームでも、レガシー改修の定石は「まずテストで囲ってから触る」でした(Michael Feathers『レガシーコード改善ガイド』の考え方)。AIエージェント時代もこの定石はそのまま効きます。違うのは、テストで囲う作業そのものをAIに高速にやらせられる点です。


完成イメージ:このやり方で手に入るもの
この記事の手順を1周回すと、リポジトリには次のものが増えます。「コードがきれいになった」だけでなく、次の改修をAIに安全に任せられる土台が残るのがポイントです。
order-system/
├── CLAUDE.md # AIと人間の共通ルール(テストコマンド・禁止事項・用語)
├── docs/
│ ├── ARCHITECTURE.md # 構成説明・主要な処理の流れ(AIが調査して下書き→人が修正)
│ ├── DEPENDENCIES.md # モジュール依存関係図(Mermaid)と外部依存の一覧
│ └── RISKS.md # 危険箇所リスト(暗黙の仕様・副作用・テスト困難箇所)
├── tests/
│ ├── characterization/ # 特性テスト:今の振る舞いを固定(リファクタ中は変更禁止)
│ └── unit/ # リファクタ後に書く「あるべき仕様」のテスト
└── .claude/
├── settings.json # permissions と hooks(テスト自動実行・特性テスト保護)
├── hooks/ # hooks から呼ぶシェルスクリプト
└── agents/ # 調査係・テスト係・レビュー係のサブエージェント定義作業の流れは次のループです。ポイントは、ループの外側(調査・固定)に最初の時間を使い、内側(直す)は小さく何度も回すことです。
準備:Claude Codeと作業ブランチ
本記事はClaude Codeで説明しますが、考え方はCursorやCodexなど他のコーディングエージェントでも同じです(コマンドや設定ファイル名は各ツールの公式ドキュメントで確認してください)。
# Claude Code をインストール(手順・対応環境は執筆時点。公式ドキュメントで最新を確認)
npm install -g @anthropic-ai/claude-code
# 対象リポジトリで作業ブランチを切る。main で直接作業させない
cd order-system
git switch -c refactor/pricing-module
git status # 作業前にクリーンであることを確認(AIの変更と自分の変更を混ぜない)
claude- 今のテストは全部通るかを先に確認する。最初から落ちているテストがあるなら、それを記録してから始める(AIが「自分が壊した」と誤解して迷走するのを防ぐ)
- ローカルで動かせるか。DBやバッチが本番にしかないなら、まず動かせる環境づくり自体をタスクにする
- 秘密情報をAIの読み取り対象から外す(
.env、本番接続情報)。後述の permissions で deny する
実践ログ:架空の受注管理システムを改修する
ここからは、架空の「10年もの社内受注管理システム(Python+jQuery、テストはほぼなし)」の料金計算モジュールを改修する想定で、AIとのやりとりを時系列で再現します。実在の案件ではなく、よくある構成を組み合わせた説明用の例です。
STEP 1|いきなり直させない:まず「読ませて文書化」させる
最初のプロンプトで一番大事なのは、「コードを変更しないで」と明言することです。Plan Mode(Shift+Tabで切り替え。ファイルを変更せず調査と計画だけを行うモード)で始めると確実です。
このリポジトリの改修を始める前に、現状を理解したい。コードは一切変更しないで。
1. 全体構成を調べて docs/ARCHITECTURE.md の下書きを作る計画を出して。
- エントリポイント、主要モジュールの責務、リクエストから DB 更新までの処理の流れ
2. モジュール間の依存関係を Mermaid の graph で docs/DEPENDENCIES.md に。
- 静的に追えない依存(動的 import、文字列で組み立てる SQL、テンプレートから呼ばれる関数、
グローバル変数)は「要確認」として別リストにする
3. 危険箇所を docs/RISKS.md に。特に pricing/ 配下について:
- 暗黙の仕様に見える処理(端数処理、NULL と空文字の区別、日付の境界、税率)
- 副作用(DB 書き込み、メール送信、外部 API、ファイル出力)
- テストが書きにくい理由(現在時刻依存、乱数、グローバル状態)
推測で書いた部分には必ず「推測」と明記して。根拠にしたファイルと行も添えて。Claude Codeがしたこと:ディレクトリ構成を一覧し、grepで呼び出し元を検索しながら主要ファイルを読み、計画を提示。承認するとdocs配下に3ファイルを作成しました。RISKS.mdには例えば次のような項目が並びます。
## pricing/calc.py
- [暗黙の仕様] calc_price() は席数 11 以上で 1 席あたり単価を切り替えるが、
境界が「>= 11」か「> 10」かで浮動小数の丸め順が異なる(calc.py L88-L104)
- [暗黙の仕様・推測] クーポン "EXPIRED" 系は例外ではなく 0 円割引として扱われる。
請求書の再発行処理がこの挙動に依存している可能性(billing/reissue.py L41 から呼び出し)
- [副作用] apply_campaign() がグローバルな CAMPAIGN_CACHE を書き換える
- [テスト困難] datetime.now() を直接呼んでいる(calc.py L17, L130)→ 時刻の注入が必要
## 静的に追えない依存(要確認)
- templates/order.html から {{ pricing.display_price(...) }} で呼ばれている人間がすること:このdocsを読んで間違いを直す。ここが最初のレビューポイントです。AIの「推測」を知っている人に確認し、事実に書き換えます。この作業で、チームの暗黙知がはじめて文書になります。レガシー改修では、この副産物だけでも価値があります。
STEP 2|CLAUDE.md を整備する
/initでCLAUDE.mdの雛形を生成し、調査結果を踏まえて人間が「このリポジトリのルール」を足します。CLAUDE.mdは毎回読み込まれるので、長い説明ではなく、守ってほしい制約と手順を短く書くのが効きます。
# order-system 作業ルール
## コマンド
- テスト(速い): pytest -q tests/characterization -x
- テスト(全体): pytest -q
- リンタ: ruff check .
- 起動: make dev(DB は docker compose up -d db)
## リファクタリング中の絶対ルール
- tests/characterization/ は「現在の振る舞いの記録」。変更・削除・skip 禁止。
振る舞いを変える必要があると判断したら、作業を止めて理由を報告すること
- 1 回の変更は 1 つの意図だけ。依頼されていないファイルの整形・改名・コメント修正をしない
- 新しい抽象(基底クラス・ファクトリ・汎用ユーティリティ)を導入する前に、必ず提案して承認を得る
- 公開関数のシグネチャを変えない(templates/ から文字列で呼ばれている箇所がある。docs/DEPENDENCIES.md 参照)
## 用語
- 席(seat)= 契約ユーザー数。 プラン = free / basic / pro
- 「請求書の再発行」は billing/reissue.py。料金計算の挙動変更は請求額に直結する
## 参照
- 構成: docs/ARCHITECTURE.md / 危険箇所: docs/RISKS.md
STEP 3|特性テストで「今の振る舞い」を固定する
pricing/calc.py の calc_price() について、特性テスト(characterization test)を作って。
目的は「正しい仕様の確認」ではなく「今の挙動を記録して、リファクタで変わったら検知する」こと。
- 入力の組み合わせ: plan(free/basic/pro)× seats(0, 1, 10, 11, 100)× coupon(None, "WELCOME10", "EXPIRED", 不正な文字列)
- 例外が出る入力は、例外の型を期待値として記録する
- まず scripts/record_calc_price.py で現在の出力を JSON に記録し、
tests/characterization/test_calc_price.py はその JSON と比較するだけにして
- datetime.now() 依存がある場合は、テスト内で固定する方法を提案して(本体は変更しないで)
- 「バグっぽい挙動」を見つけても直さず、docs/RISKS.md に追記するだけにしてClaude Codeがしたこと:記録用スクリプトとテストを作成し、スクリプトを実行してJSON(60ケース)を生成、pytestで全件パスを確認。さらに「seats=0 のとき basic プランが負の金額を返す」という挙動を見つけ、直さずにRISKS.mdへ追記して報告しました(架空の例)。この「直さずに報告」を指示しておくことが重要です。バグ修正とリファクタを同じ変更に混ぜると、差分レビューで「挙動が変わったのは意図か事故か」が判別できなくなります。
STEP 4|Plan Modeで計画→小さな単位で直す
calc_price() を読みやすくしたい。挙動は一切変えない前提で、リファクタリング計画を出して。
- 1 ステップ = 1 コミットになる粒度で、順番に並べて
- 各ステップで「何を変えるか」「どのテストで安全を確かめるか」「リスク」を書いて
- 新しいクラスやモジュールを作る案は、作らない案と比較して理由を書いてAIは例えば次のような計画を返します。人間はここで粒度と順番をレビューします。
| ステップ | 変更内容 | 確認方法 | 判断 |
|---|---|---|---|
| 1 | datetime.now() を引数 now(デフォルトNone)で注入可能に | 特性テスト全件 | 採用:テスト容易性が上がり挙動不変 |
| 2 | 席数による単価分岐を関数 _unit_price() に抽出 | 特性テスト全件 | 採用 |
| 3 | クーポン処理を _apply_coupon() に抽出 | 特性テスト全件 | 採用 |
| 4 | PricingStrategy 基底クラス+プランごとのサブクラスを導入 | 特性テスト全件 | 却下:プランは3つで増える予定がない。抽象の維持コストの方が高い |
| 5 | float を Decimal に変更 | — | 別タスクへ:丸め結果が変わる=挙動変更。リファクタに混ぜない |
計画を承認したら、1ステップずつ実行させます。「全部やって」と頼むと、途中で失敗したときにどこで壊れたか分からなくなります。
計画のステップ 1 だけ実行して。終わったら pytest -q tests/characterization を実行し、
結果と git diff --stat を見せて。コミットはまだしないで。Claude Codeがしたこと:calc.pyだけを編集し、テストを実行(hooksでも自動実行される。後述)。全件パスと差分統計(1 file changed)を報告。人間が差分を読み、問題なければ「コミットして。メッセージは『refactor: calc_price に now を注入可能にする(挙動不変)』」と指示。これをステップ3まで繰り返します。
STEP 5|リファクタ後に「あるべき仕様」のテストを書く
特性テストは「今の挙動」の写しなので、バグも含めて固定しています。構造が整って読みやすくなったら、人間が仕様を決めて、tests/unit/に「あるべき振る舞い」のテストを書かせます。STEP 3で見つかった「seats=0で負の金額」を直すのはこの段階で、挙動変更として別のPRにします。このとき初めて、特性テストの該当ケースの期待値を人間の承認つきで更新します。
特性テストの書き方(コード抜粋と解説)
特性テストは「ゴールデンマスター(golden master)」「承認テスト(approval test)」とも呼ばれる手法の一種です。現在の出力を記録し、以後はその記録と一致するかだけを見る。仕様書がなくても書けるのが最大の利点です。
"""現在の calc_price() の出力を記録する。リファクタ開始前に 1 回だけ実行する。"""
import itertools
import json
import pathlib
from datetime import datetime
from pricing.calc import calc_price
FIXED_NOW = datetime(2026, 9, 1, 10, 0, 0) # 現在時刻依存を固定
PLANS = ["free", "basic", "pro"]
SEATS = [0, 1, 10, 11, 100] # 境界値(10 と 11)を必ず含める
COUPONS = [None, "WELCOME10", "EXPIRED", "!!invalid"]
cases = []
for plan, seats, coupon in itertools.product(PLANS, SEATS, COUPONS):
args = {"plan": plan, "seats": seats, "coupon": coupon}
try:
expected = {"value": repr(calc_price(**args, now=FIXED_NOW))}
except Exception as e: # 例外も「今の振る舞い」として記録する
expected = {"error": type(e).__name__}
cases.append({"id": f"{plan}-{seats}-{coupon}", "args": args, "expected": expected})
out = pathlib.Path("tests/characterization/calc_price_cases.json")
out.write_text(json.dumps(cases, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"recorded {len(cases)} cases -> {out}")import json
import pathlib
from datetime import datetime
import pytest
from pricing.calc import calc_price
FIXED_NOW = datetime(2026, 9, 1, 10, 0, 0)
CASES = json.loads(
(pathlib.Path(__file__).parent / "calc_price_cases.json").read_text(encoding="utf-8")
)
@pytest.mark.parametrize("case", CASES, ids=[c["id"] for c in CASES])
def test_calc_price_behavior_is_unchanged(case):
try:
actual = {"value": repr(calc_price(**case["args"], now=FIXED_NOW))}
except Exception as e:
actual = {"error": type(e).__name__}
assert actual == case["expected"]解説とトレードオフ:
repr()で記録する理由:floatの0.1+0.2のような誤差や、Decimal('10.00')とDecimal('10.0')の違い(等値比較では同じでも表示は違う)まで固定するため。逆に言えば「表現の違い」でも落ちるので、厳しすぎると感じたら比較方法を緩める判断を人間がする- 時刻の注入:この例では、STEP 4のステップ1で
now引数を足した後の形です。本体を一切変えられない最初の段階では、freezegun等のライブラリやunittest.mock.patchでdatetimeを差し替えて記録します。どちらを選ぶかは「本体に最小の変更を入れてテスト容易性を得る」か「本体に触らず外から固定する」かのトレードオフで、依存が深いほど後者は脆くなります - 入力の選び方:網羅ではなく境界値と分岐。AIにカバレッジ(
pytest --cov)を測らせ、「特性テストで通っていない分岐」を列挙させて入力を足すと効率的です - 記録は1回だけ:記録スクリプトをリファクタ途中で再実行すると、壊れた挙動を“正解”として上書きしてしまいます。後述のhooksで記録ファイルを保護します
toMatchSnapshot()、Playwrightのスクリーンショット比較もこの仲間です。ただしスナップショットはAIが-u(更新)で一発で“通して”しまえるので、更新は人間の承認事項にしておきます。
hooksとpermissionsでガードレールを敷く
CLAUDE.mdは「お願い」です。長い作業の途中で守られないこともあります。本当に破られたら困るルールは、Claude Codeのhooks(ツール実行の前後などに任意のシェルコマンドを走らせる仕組み)で強制します。
{
"permissions": {
"allow": [
"Bash(pytest:*)",
"Bash(ruff check:*)",
"Bash(git diff:*)",
"Bash(git status)",
"Bash(git log:*)"
],
"ask": ["Bash(git commit:*)"],
"deny": [
"Read(./.env)",
"Read(./config/production.*)",
"Bash(git push:*)",
"Bash(python scripts/record_*:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/protect-characterization.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/fast-checks.sh" }]
}
]
}
}#!/usr/bin/env bash
# PreToolUse: 特性テストの編集をブロックする(exit 2 でブロックし、stderr が Claude に返る)
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*/tests/characterization/*)
echo "tests/characterization/ はリファクタ中は変更禁止です。挙動を変える必要があるなら、作業を止めて理由を人間に報告してください。" >&2
exit 2
;;
esac
exit 0#!/usr/bin/env bash
# PostToolUse: Python ファイルを編集したら、リンタと特性テストを自動実行
# 失敗したら exit 2 で結果を Claude に返し、自分で直させる
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
ruff check "$file" 1>&2 && pytest -q -x tests/characterization 1>&2 || exit 2
;;
esac
exit 0設計の判断基準:
- hooksで回すのは「速いチェック」だけ:編集のたびに全テスト(数分)を回すと、AIの作業が極端に遅くなりコストも増えます。hooksは数秒で終わるリンタと特性テストに絞り、全テストはコミット前(またはCI)で回す。遅いテストを1つのhookに入れるくらいなら、テストの分割を先にやる方が得です
- ブロックはPreToolUse、フィードバックはPostToolUse:PreToolUseのexit 2は操作そのものを止める。PostToolUseは既に編集済みなので「結果を返して直させる」用途。役割を混ぜない
- 記録スクリプトの実行をdeny:特性テストの期待値を再生成されると保護が無意味になるため、記録系スクリプトの実行自体を禁止しています。期待値の更新は人間が手で行う
git pushはdeny、git commitはask:コミットは差分を見た上で人間がOKを出す。pushはAIに渡さない。どこまで自動にするかはチームの成熟度次第ですが、最初は厳しく始めて緩める方が安全です
hooksのイベント名・入力JSONの形式・exit codeの意味は、執筆時点(2026年9月)のClaude Code公式ドキュメントに基づいています。バージョンで変わることがあるので、導入時に公式ドキュメントで確認してください。手元で使う前に、わざとテストを壊した変更をさせてhookが本当に止めるかを試しておくのが確実です。
サブエージェントとgit worktreeで文脈と作業場所を分ける
サブエージェント:調査係・テスト係・レビュー係
サブエージェントは、別の文脈(コンテキスト)で動く役割別のエージェントです。.claude/agents/<name>.mdに定義します。レガシー改修で効くのは次の3役です。
| 役割 | やること | 渡すツール | 分ける理由 |
|---|---|---|---|
| 調査係 | 呼び出し元・依存・影響範囲を調べて要約だけ返す | Read, Grep, Glob | 大量のファイルを読んだ“生の文脈”でメインを汚さない。結論だけ受け取る |
| テスト係 | 特性テスト・単体テストを書く | Read, Grep, Glob, Write, Bash | 実装した本人がテストを書くと「実装に合わせたテスト」になりやすい |
| レビュー係 | 差分を読み、後述の観点で指摘する(修正はしない) | Read, Grep, Glob, Bash | 書いた文脈を持たない“第三者の目”で見る。Edit権限を与えない |
---
name: diff-reviewer
description: リファクタリングの差分をレビューする。コミット前やPR作成前に使う。コードは修正しない。
tools: Read, Grep, Glob, Bash
---
あなたはレガシーコード改修のレビュアーです。`git diff main...HEAD` を読み、次の観点で指摘してください。
修正はせず、指摘を「重大 / 要確認 / 軽微」に分けて、ファイル名と行を添えて報告します。
1. テストの弱体化: assert の削除・緩和、skip/xfail の追加、期待値の書き換え、例外の握りつぶし
2. スコープ外の変更: 依頼の意図と関係ないファイル・整形・改名・コメント変更
3. 過剰な抽象化: 呼び出し元が1つしかない基底クラス・ファクトリ・設定項目の追加
4. 挙動変更の混入: 丸め・日付境界・NULL の扱い・例外の型・ログ/副作用の順序の変化
5. 公開インターフェースの変更: 関数シグネチャ、テンプレートから呼ばれる関数名
最後に「このままコミットしてよいか」を Yes/No と理由1行で答えてください。トレードオフ:サブエージェントは文脈を分けられる代わりに、メインとの情報の受け渡しが「要約」になるので細部が落ちます。また呼び出すたびにトークンを消費します。小さな改修なら1つの会話で十分で、「読むファイルが多い」「独立した視点が欲しい」ときに使うのが目安です。会話が長くなってきたら/clearで文脈をリセットし、docsとCLAUDE.mdから読み直させる方が、サブエージェントを増やすより効くこともあります。
git worktree:並列作業の作業場所を分ける
同じリポジトリの別ブランチを、別ディレクトリに同時に取り出せるのがgit worktreeです。AIエージェントを複数並列で走らせるとき、同じ作業ツリーで2つのエージェントが同時にファイルを書き換える事故を防げます。
# 独立したモジュールごとに作業場所を分ける
git worktree add ../order-wt-pricing -b refactor/pricing
git worktree add ../order-wt-report -b refactor/report-export
# それぞれのディレクトリで別の Claude Code セッションを起動
cd ../order-wt-pricing && claude
# (別ターミナル) cd ../order-wt-report && claude
# 終わったら片付け
git worktree remove ../order-wt-pricing大規模移行の分割戦略
jQuery→素のJS/React、JavaScript→TypeScript、Python 2→3のような移行は、「全部書き換えて一気に切り替える(ビッグバン)」と「共存させながら少しずつ置き換える(段階移行)」のどちらかを選ぶことになります。AIエージェントがいると、ビッグバンの書き換え自体は速く終わるように見えます。しかし検証とレビューの量は減らないので、原則は段階移行です。
| 移行 | 分割の単位 | 共存のさせ方 | AIに向く作業/人が握る判断 |
|---|---|---|---|
| jQuery → 素のJS / React | 画面の部品(ウィジェット)単位 | ストラングラー(絞め殺しの木)パターン:既存ページの一部の要素にだけReactをマウントし、端から置き換える | AI:$(...)の置き換え、イベント処理の移植、E2Eテスト作成/人:状態管理の方針、どこでReactに切り替えるか |
| JavaScript → TypeScript | ファイル単位(依存の葉から) | allowJsで.jsと.tsを共存。strictは新規・移行済みファイルから | AI:型注釈の追加、anyの削減/人:anyや@ts-expect-errorを許す基準、外部データの型の決め方 |
| Python 2 → 3 型の言語・ランタイム移行 | モジュール単位 | 両バージョンで動く書き方の期間を設け、CIで両方テスト | AI:機械的な構文変換、テストの両対応/人:文字列とバイト列の境界、切り替え日 |
| フレームワークのメジャー更新 | 非推奨APIごと | 非推奨警告を1種類ずつ潰す | AI:警告の一覧化と置換/人:破壊的変更の影響判断 |
JS→TSの場合、最初の設定は次のような形が定番です。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"checkJs": false,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}allowJs: trueで既存の.jsをそのまま取り込み、.tsに変えたファイルからstrictの型チェックがかかります。AIには「依存の葉にあるファイルを1つだけ.tsにして、npx tsc --noEmitを通して」と1ファイルずつ頼みます。進捗の指標として、anyと@ts-expect-errorの件数を毎回数えさせると、AIが型エラーをanyで“黙らせて”いないかが見えます。
src/ 配下の JavaScript を TypeScript に段階移行したい。コードはまだ変えないで、計画だけ出して。
- import の依存グラフを調べ、他から依存されていないファイル(葉)から順に並べた移行順リストを作る
- 1 PR あたりの目安は 5 ファイル以内。PR ごとの移行対象を分けて
- 外部 API のレスポンスや localStorage など「実行時にしか形が分からないデータ」は一覧にして、
型をどう決めるか(zod 等で実行時検証するか、型だけ付けるか)の選択肢を比較して
- 各 PR の完了条件: npx tsc --noEmit と npm test が通る / any と @ts-expect-error の件数を報告

差分レビューの観点:AIのやりがちを見抜く
AIエージェントの差分レビューでは、人間のコードレビューとは少し違う観点が必要です。AIは「テストを通す」「依頼を完了する」ことに強く最適化されるため、目的と手段が入れ替わった変更が紛れ込みます。代表的な3つと、見抜き方をまとめます。
| やりがち | 具体例 | 見抜き方 |
|---|---|---|
| テストを弱めて通す | assertの削除、toEqual→toBeTruthy、skip/xfail追加、スナップショットの一括更新、テスト対象そのものをモック化、try/exceptで例外を握りつぶす | テストファイルの差分を最初に読む。削除行(-)にassertがないか機械的に検索する |
| 関係ない箇所の書き換え | ついでの整形、変数名の改名、コメントの“改善”、import順の並べ替え、依頼外のバグ修正 | git diff --statで変更ファイル数・行数が想定と合うか。フォーマッタは別コミットに分ける |
| 過剰な抽象化 | 呼び出し元が1つのインターフェース、使われない設定項目、汎用ユーティリティ、将来のための拡張ポイント | 新規ファイル・新規クラスの一覧を見る。「これを消したら何が困るか」を問う |
機械的に拾えるものは、スクリプトにしてレビュー係のサブエージェントやCIに組み込むと漏れません。
#!/usr/bin/env bash
# リファクタ差分の「怪しい変更」を機械的に拾う(完全ではない。人のレビューの補助)
base=${1:-main}
echo "== 変更規模 =="
git diff --stat "$base"...HEAD | tail -1
echo "== テストから消えた assert / expect =="
git diff "$base"...HEAD -- 'tests/**' '*.test.*' '*_test.py' \
| grep -E '^-[^-].*(assert|expect\()' || echo "(なし)"
echo "== 追加された skip / only / 型・lint の黙らせ =="
git diff "$base"...HEAD \
| grep -E '^\+[^+].*(\.skip\(|\.only\(|xit\(|pytest\.mark\.(skip|xfail)|@ts-ignore|@ts-expect-error|eslint-disable|# noqa|: any\b)' \
|| echo "(なし)"
echo "== 新規ファイル(過剰な抽象化の候補) =="
git diff --name-status "$base"...HEAD | grep '^A' || echo "(なし)"その上で、人間は次の順番で読むと効率的です。①テストの差分 → ②変更ファイル一覧(想定外がないか) → ③新規ファイル → ④本体のロジック。ロジックから読み始めると、「よく書けている」という印象に引っ張られて、テストの弱体化を見落としがちです。
つまずきポイントとAIへの直し方の頼み方
| 症状 | 原因 | 頼み方の例 |
|---|---|---|
| テストが落ちたら、AIがテストの方を直してきた | 「テストを通して」という目的の伝え方 | 「テストは仕様の記録なので変更禁止。落ちた原因を本体コードの側で特定して。本体が正しくテストが古いと思うなら、直さずに根拠を説明して」 |
| 同じ修正を行ったり来たりする | 会話が長くなり文脈が混線 | /clearしてから「docs/RISKS.mdとCLAUDE.mdを読み直して。現在の失敗はこれ:(エラー全文)。仮説を3つ挙げ、検証方法とセットで出して」 |
| 差分が20ファイル以上に広がった | 依頼の粒度が大きい/ついで修正 | Escで中断→git restore .で戻す→「対象はpricing/calc.pyだけ。他のファイルに変更が必要なら、変更せずに一覧で報告して」 |
| 「完了しました」と言うが動かない | 検証手段を指定していない | 「完了の定義:pytest -qが全件パスし、make devで起動して/ordersが200を返すこと。確認したコマンドと出力を貼って」 |
| 存在しない関数やオプションを使う | 古いバージョンの知識で書いている | 「requirements.txtのバージョンを確認し、そのバージョンのドキュメント/ソースで該当APIの存在を確かめてから使って」 |
共通するコツは、「何をしてほしいか」より「何をもって完了とするか」「何をしてはいけないか」を先に言うことです。そして、やり直しは叱るより戻す方が速い。Gitで小まめにコミットしておけば、AIの失敗はgit restoreやgit resetで数秒でなかったことにできます。
事例:本サイトの一括改修/他社の公開事例
全56ページのヘッダー統一・全ページへのAdSense追加をAIエージェントで一括実施
このサイト(HTML+CSSのみの静的サイト、約57ページ)は、Claude Codeとの共同作業で作成・運用しています。コミット履歴の大半にAIエージェントとの共同作業の記録(Co-Authored-By)が残っています。その中から、本記事のテーマに近い「横断的な一括変更」の例を紹介します(内容はgit logで確認できる事実です)。
- ヘッダーの導線別メニュー統一(2026年9月):全ページのヘッダーを「ホーム/学ぶ/活かす/キャリア…」の導線別メニューに統一し、未整備だったSNS導線をヘッダー・フッターから削除、CSSのキャッシュ用バージョン文字列も全ページで揃えました。1コミットで57ファイル変更。直後に「活かす→活用」などのメニュー調整を全56ページに適用する小さなコミットを重ねています
- AdSense審査コードとads.txtの追加:全42ページ(当時)の<head>に審査コードを挿入し、ads.txtを新規作成、プライバシーポリシーに広告配信の項目を追記。コミットメッセージに「運営会社のサービスサイトには一切入れていない」と対象外の範囲まで明記しています
こうした一括変更を支えたのが、AIと一緒に作った機械チェックです。
- build.js:共通フッター(アフィリエイト表記を含む)を全HTMLの
<!-- FOOTER:START -->〜<!-- FOOTER:END -->間に流し込むNode.jsスクリプト。マーカーが壊れている・フッターを入れられないページが1つでもあるとexit 1で止まり、問題のページ名と直し方を表示します。一括置換でマーカーを巻き込んで壊しても、公開前に気づける - keihyo-check.js:PR表示の有無と位置、★評価の根拠注記、
target="_blank"のnoopenerなどを検査し、HARD違反があればexit 1 - 差分の規模確認:「全ページのヘッダー変更」なら、変更ファイル数がページ数と一致するかを
git diff --statで見る。多すぎればスコープ外の変更、少なければ置換漏れのサイン
作業は作業ブランチで行い、PRで人が確認してからマージする流れです。静的サイトの一括変更は「レガシー改修」より単純ですが、“全ページに同じ変更を入れたはず”を人の目ではなく機械で保証するという構造は同じです。運用の全体像は【自社事例】AIエージェントでメディアサイトを運用する仕組みで公開しています。
他社の公開事例
- 楽天:AnthropicがClaude 4を発表した際(2025年5月)、楽天がClaude Opus 4を約7時間にわたって自律的に動かし、オープンソースプロジェクトのリファクタリングを行った事例が紹介されました。長時間の自律作業が成立するには、途中経過を検証できる仕組み(テスト)が前提になる、という点で本記事のループ設計と同じ方向の事例と言えます(詳細な条件は公開情報の範囲でしか分かりません)
- Anthropic:社内でClaude Codeを広く使い、Claude Code自体のコードの多くをClaude Codeで書いていると公表しています
いずれも「AIに丸投げした」のではなく、検証の仕組みと人のレビューを前提にした上での自律実行である点を読み違えないようにしたいところです。
発展アイデア
- CIで同じチェックを回す:hooksは手元のエージェント作業のガード。PRごとにGitHub Actionsで特性テスト・リンタ・
suspicious-diff.shを実行し、失敗したらマージできないようにすると、人やAIの注意力に依存しなくなります - ミューテーションテストで特性テストの強さを測る:本体をわざと少し壊して(mutmut、Stryker等)、特性テストが検知できるかを見る。検知できない変異が多い箇所は入力を足す
- ヘッドレス実行で定型リファクタを量産:
claude -p "…"で非対話実行し、非推奨APIの置換のような定型作業をモジュールごとに流す。ただし結果はPRとして人がレビューする - RISKS.mdを育てる:改修のたびに見つかった暗黙の仕様を追記していくと、次の改修の最初の「読ませる」工程が短くなります
コマンドやGitの基本から押さえたい方は、IT基礎ロードマップ(特に第7章 Git・GitHub・デプロイ)から読むのがおすすめです。
よくある質問
テストが1本もないレガシーコードでも、AIエージェントにリファクタリングを任せられる?
任せられますが、最初の仕事は「直すこと」ではなく「特性テストで今の振る舞いを記録すること」にしてください。特性テストは仕様書がなくても、現在の出力を記録して比較するだけなので書けます。テストで囲ってから小さく直し、テストが通ることを確認してコミットする、というループを回せば、テストゼロの状態から安全に始められます。
特性テストと普通の単体テストは何が違う?
単体テストは「あるべき仕様」を確かめるテストで、特性テストは「今の振る舞い」を記録して変化を検知するテストです。特性テストはバグも含めて固定するため、リファクタリング中の安全網として使い、構造が整った後に人間が仕様を決めて単体テストを書き、バグ修正は挙動変更として別の変更に分けます。
AIがテストを書き換えて通してしまうのを防ぐには?
CLAUDE.mdで禁止するだけでなく、Claude CodeのhooksのPreToolUseで特性テストのディレクトリへの編集をブロックし、期待値を再生成するスクリプトの実行もpermissionsでdenyします。さらに差分レビューではテストファイルの差分を最初に読み、削除されたassertや追加されたskipを機械的に検索します。
大規模な移行は一気に書き換えた方が速いのでは?
書き換えそのものはAIなら速く終わりますが、検証とレビューの量は減りません。数千行の差分は人が責任を持って読めず、実質的に本番で初めてテストすることになります。原則は、共存させながら依存の葉から置き換える段階移行で、1つの変更を人がレビューできる量に分割することをおすすめします。
まとめ|AIに「速く直させる」前に「安く確かめられる」状態を作る
レガシー改修でAIエージェントを戦力にする順番は、読ませる → ルール化する → 振る舞いを固定する → 小さく直す → 機械で確かめる → 人が差分を見てコミットです。AIの書く速度はすでに十分速い。成果を分けるのは、「壊していない」と数秒で確信できる仕組みと、人がレビューできる大きさに作業を切る判断です。
テストを弱めて通す・関係ない箇所を書き換える・過剰に抽象化する——AIのやりがちは、お願いではなくhooksと差分チェックで止める。そうして整えたリポジトリは、次の改修をさらに安全にAIへ任せられる資産になります。次の記事では、ゼロから1人でSaaSのMVPを作る場面で、同じ「機械チェックで止める」考え方を仕様書駆動・CIまで広げます。
作れるようになったら、スキルを収入につなげる
AIエージェントで「作れる」ことは、それ自体が仕事になります。案件獲得や転職、作業環境づくりに役立つ記事をまとめました。
- AIで独立するには?未経験から案件を取る6ステップ【2026】:自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
- 仕事用の外付けモニターおすすめ3選【2026】:エディタ・ターミナル・ブラウザを並べると作業効率が段違い