「このディレクトリ一式を別の構造に書き直して」と Claude Code に頼み、数十秒後に返ってきた 2,000 行の diff を前に手が止まる——個人開発でも受託でも、私は何度かこの瞬間を味わってきました。生成は速いのに、レビューに必要な集中時間は差分の大きさに対して指数関数的に伸びていきます。
しばらくは気合いでレビューしていましたが、個人開発者として複数プロジェクトを並行で持つようになって、その方針は明確に行き詰まりました。今は順序を逆にしています。コードを生成させる前に「どこまで戻せば確実に動くか」を先に設計し、その粒度を Claude Code に守らせる。この記事は、そのために実際に運用しているマニフェスト・フック・ロールバック検知の中身を、コピーして使える形でまとめた作業メモです。
テストが緑でも壊れる層をまず見積もる
Claude Code は賢いので、たいていのリファクタは「動くコード」を返してきます。厄介なのは、動くことと正しく動くことの距離が、差分が大きいほど開いていく点です。
私が実際に踏んだのは、単体テストでは絶対に拾えない層でした。データベース接続の初期化順序が 1 行ずれていて、デプロイ後のアイドル時だけコネクションが枯渇したことがあります。別の案件では、既存コードが「例外を握りつぶして既定値を返す」前提に依存していたのを素直に throw する形へ直してしまい、夜間バッチが丸ごと止まりました。どちらも、既存コードが暗黙に張っていた契約を新しい差分が壊したのに、テストはその契約を表現していなかったために緑のまま通ってしまったのです。
ここから持ち帰った結論は一つです。リファクタの規模とレビュー可能性は別物として設計しないと破綻する。大きく書き直すこと自体は構いません。問題は、それを一度に渡され、一度に検証しなければならない状態に置かれることです。
チェックポイントを「マニフェスト」として先に宣言する
リファクタを始めるとき、私が最初にやるのはコード生成ではなく、戻せる点を地図に打つことです。これをコメントや口頭ではなく、リポジトリに置く YAML マニフェストにしておくと、後段のフックやレビューがそれを参照できるようになります。
# refactor.checkpoints.yml — リファクタ開始前に確定する
target : OrderService を境界を分けた構造へ寄せ替える
rollback_signals :
p95_latency_ms : 450 # これを超えたら直前 CP へ戻す
error_rate_pct : 1.0
checkpoints :
- id : CP1
intent : interfaces/ と usecases/ を新規追加。既存 OrderService は 1 行も変更しない
invariant : 既存コードからの呼び出しは一切発生しない(純粋な追加のみ)
- id : CP2
intent : 新 UseCase から既存 OrderService を呼ぶアダプタを追加
invariant : 入口 Controller は旧経路を既定にしたフラグ分岐で両対応
- id : CP3
intent : テストを UseCase 側へ移植。旧テストは残す。フラグは既定 false
invariant : フラグ false の挙動は CP2 と完全一致
- id : CP4
intent : 本番トラフィックの一部でフラグを true に。問題なければ既定を反転
invariant : いつでもフラグ false へ即時復帰できる
- id : CP5
intent : 旧 OrderService とフラグ分岐を削除
invariant : CP4 が本番で安定したことが前提
このマニフェストの効きどころは invariant(各チェックポイントで守られているべき不変条件)です。「何をするか」だけでなく「終わった時点で何が壊れていないと言えるか」を先に言語化しておくと、Claude Code への依頼が「まるごと書き直して」から「CP1 の invariant を満たす差分だけ作って」へと自然に変わります。大きな diff が返ってくるのは Claude Code のせいではなく、こちらが粒度を定義していないからだ、と気づいたのがこの運用の出発点でした。
プロンプトに粒度の契約を埋め込む
マニフェストが決まったら、その 1 チェックポイント分だけをプロンプトに渡します。私が使っているテンプレートは、範囲を越えそうなときに「実装せず提案で止まる」ことを明示的に求める形です。
あなたはこのリポジトリで refactor.checkpoints.yml に沿ったリファクタを行います。
今回のタスクは CP1 の範囲のみです。
制約:
1. CP1 の intent と invariant だけを満たす差分を作る。
既存 OrderService のコードは 1 行も変更しない。
2. CP1 の範囲を超える変更が必要だと判断したら、実装せず
「CP2 として提案します」と明記し、理由を 2〜3 行で書く。
3. 出力は次の JSON で返す:
{
"diff_summary": "変更点の要約",
"invariant_check": "CP1 の invariant を破っていないことの根拠",
"almost_broke": "範囲外に手を出しかけて踏みとどまった箇所(必ず1つ以上)"
}
almost_broke を必ず 1 つ以上書かせるのが要点です。Claude Code は気を利かせて隣接する改善を混ぜ込んでくることがあり、コードが綺麗になるので受け入れたくなります。しかしそれを許すと 1 コミットの意味が広がり、可逆性が崩れます。範囲外の改善は喜ばず、別チェックポイントへ回す。受託案件で何度か比べた限り、この線引きを徹底したプロジェクトの方が、結果としてリファクタ完遂までの総時間は短くなりました。途中で問題が出ても 1 コミット単位で git revert が効くからです。
pre-push フックで粒度を機械的に止める
プロンプトで粒度を頼んでも、人間が「まあ今回はいいか」と通してしまえば崩れます。そこで、巨大 diff とチェックポイント未記載のコミットを物理的に push できないようにします。次のフックを .git/hooks/pre-push に置いています。
#!/usr/bin/env bash
# .git/hooks/pre-push — 大規模リファクタの粒度を機械的に守らせる
set -euo pipefail
MAX_LINES = 300
range = "origin/main..HEAD"
# 1) 各コミットがチェックポイント ID を含むか
bad_msg = $( git log --format= '%H %s' $range | grep -viE 'CP[0-9]+' || true )
if [ -n " $bad_msg " ]; then
echo "✗ チェックポイント ID (CP1 等) のないコミットがあります:"
echo " $bad_msg "
exit 1
fi
# 2) 1 コミットあたりの変更行数が上限を超えていないか
while read -r sha ; do
lines = $( git show --stat --format= '' " $sha " | tail -1 | grep -oE '[0-9]+ (insertion|deletion)' \
| grep -oE '[0-9]+' | paste -sd+ - | bc )
lines = ${lines :- 0}
if [ " $lines " -gt " $MAX_LINES " ]; then
echo "✗ コミット ${ sha : 0 : 8 } は ${ lines } 行(上限 ${ MAX_LINES })。分割してください。"
exit 1
fi
done < <( git rev-list $range )
# 3) 各コミット時点でビルドが通るか(任意・重い場合は CI 側へ)
echo "✓ 粒度チェック通過"
行数上限を 300 にしているのは経験則です。私の感覚では、集中を切らさず読み切れる差分はおおむね 300〜500 行が上限で、その下限側に寄せておくと安全側に倒れます。「コミットメッセージに CP 番号が必須」という制約も地味に効いて、マニフェストにない作業をうっかり混ぜ込むと push 段階で止まります。重いビルド検証は CI に寄せ、ローカルでは行数とチェックポイント整合だけを軽く見るのが実用的でした。
巨大 diff が出たら分割を依頼する
それでも 300 行を超える差分は普通に出ます。そのときは Claude Code に分割を頼みますが、ここで効く制約が「各コミット単体でビルド・起動できること」です。
この diff は約 XXX 行でレビューが困難です。次のルールで分割案を作ってください。
- 1 コミット目: 型定義と空実装のみ(振る舞いは変えない)
- 2 コミット目: 旧処理を新型に寄せるが、呼び出し元は変えない
- 3 コミット目: 呼び出し元を新抽象へ切り替える
- 4 コミット目: 旧実装を削除する
必須条件: 各コミットを単体で適用した時点でアプリがビルド・起動できること。
保証できない分割案は出力しないでください。
この一文を入れるだけで、Claude Code は旧実装と新実装を一時的に共存させる「ブリッジ」を自動的に挟む設計を選びます。ブリッジを後で消すコミットは短く明快なので、レビューもロールバックも楽になります。一発で理想の分割が出るわけではないので、最初の案を自分のマニフェストと突き合わせ、「CP2 と CP3 の間にもう 1 段欲しい」と対話で詰めます。Claude Code を一発屋ではなくリファクタの相棒として扱う、という感覚に近いです。
セルフレビューを契約に押し込む
コミット粒度が揃っても、レビューの密度を上げるために Claude Code 自身に弱点を吐かせます。同じセッションで続けてこう頼みます。
この変更について次の 3 点をセルフレビューしてください。
1. 既存コードが暗黙に依存していた契約で、今回壊している可能性のあるもの
(例: 例外の伝播経路・ログフォーマット・初期化順序・null の扱い)
2. テストが無いが、本番で壊れると痛い経路
3. 1・2 のうち、あなた自身が自信を持てない箇所(ゼロ回答は禁止・最低 1 つ)
3 番の「自信のなさを最低 1 つ」が鍵です。Claude Code は「問題ありません」と返すのが得意で、それではレビューに使えません。怪しいと思っている点を強制的に出させると、こちらが重点的に読むべき場所が浮かびます。私はこの出力をコミットメッセージ本文に貼り、未来の自分や他のレビュアーが「ここは作った本人も自信がないと書いている」と判断できるようにしています。
ロールバック判断を Observability に紐づける
最後は実地検証です。差分ごとにまとめてテストするのではなく、1 コミットごとに検証する癖をつけています。確認するのは、テストが緑であること、ステージング相当で主要導線を自分の目で 1 度通すこと、そしてメトリクス系列の形が変更前後で明らかに変わっていないこと、の 3 点です。
3 つ目はテストでは拾えない違和感を捕まえる網です。マニフェストの rollback_signals を使って、半自動で判定します。
# rollback_check.py — マニフェストの閾値で戻すべきか判定する
import sys, yaml, statistics
cp = yaml.safe_load( open ( "refactor.checkpoints.yml" ))
sig = cp[ "rollback_signals" ]
def p95 (series):
s = sorted (series)
return s[ min ( len (s) - 1 , int ( len (s) * 0.95 ))]
# before/after は監視基盤から取得した同区間のサンプル
before_lat = [ ... ] # デプロイ前の応答時間(ms)
after_lat = [ ... ] # デプロイ後の応答時間(ms)
after_err = ... # デプロイ後のエラー率(%)
reasons = []
if p95(after_lat) > sig[ "p95_latency_ms" ]:
reasons.append( f "p95= { p95(after_lat) } ms > { sig[ 'p95_latency_ms' ] } ms" )
if after_err > sig[ "error_rate_pct" ]:
reasons.append( f "error_rate= { after_err } % > { sig[ 'error_rate_pct' ] } %" )
# 形の変化も見る: 中央値が 20% 以上右へ寄ったら警告
if statistics.median(after_lat) > statistics.median(before_lat) * 1.2 :
reasons.append( "中央値が 20% 以上悪化(テスト緑でも要確認)" )
if reasons:
print ( "⟲ 直前チェックポイントへ戻す候補:" )
print ( " \n " .join(reasons))
sys.exit( 1 )
print ( "✓ ロールバック不要" )
閾値(p95_latency_ms など)は記事の数字をそのまま使うのではなく、変更前 1〜2 週間の実測分布から決めるのが大事です。私は平常時の p95 に 1.3 倍ほどの余裕を乗せた値を置き、それを超えたら迷わず直前のコミットへ戻します。戻すことをコストとは考えません。粒度を整えてきたのは、まさにこの瞬間に安全弁が設計どおり作動するためだからです。
速さを活かすのは、地味なサイクルを速く回せたとき
大規模リファクタを Claude Code に任せると、つい「一発で書き直す魔法」を期待します。けれど個人開発でも受託でも繰り返し感じるのは、本領は地味なサイクルを速く回すことにある、という点です。チェックポイントをマニフェストで宣言し、粒度を pre-push で機械的に守り、巨大 diff は共存ブリッジで分割し、セルフレビューで弱点を吐かせ、ロールバックは Observability の数字で淡々と判断する。どれも特別な技術ではありません。
次にリファクタへ取りかかるときは、コードを書かせる前に refactor.checkpoints.yml を 5 行だけ書いてみてください。そこから Claude Code への頼み方が変わり、週明けに本番が壊れていないか怖くて眠れない夜が、静かに減っていきます。同じ課題に向き合っている方の役に立てば嬉しいです。