Free

Claude にリファクタリングをやらせる

リファクタには症状がない。Claude が外しても見えない。テスト、原子コミット、手動クリックの 3 本のガードレールで押さえる。


リファクタリングは Claude にとって一番危険なタスクだ。バグには症状がある——ボタンが反応しない、値が undefined、スタックトレース——だから Claude の修正が正しいかどうか判断できる。リファクタリングには症状がない。「まだ動く」は「テストが通る」と同じ意味になりうる、裏で挙動が変わっていて、気づかず、一週間後に本番が炎上する。

最近 how2claude で Claude に大きめのリファクタリングをやらせた:x402 暗号決済を、自作の PaymentHandler + FacilitatorClient(139 行)から x402-rails gem に移行、同時に Purchase.create! / Subscription.create! の——二つのコントローラに散らばって重複していた——フィールドマッピングを model のクラスメソッドに抽出。1 コミット、4 ファイル変更、2 削除、2 追加。

プロンプトは一語だった:「リファクタリング」。

こんなに短くて済んだのは、周囲にガードレールがあったから。

ガードレール #1:テストがない状態でリファクタリングしない

そこまで来た時点でこのブランチにはテストが 221 本。決済フローのクリティカルパスは全部カバー済みだった。

Claude のデフォルト動作は「まずテストを見る」ではない。だから先に bin/rails test を走らせてグリーンを確認してから手を付けろと指示する。

リファクタリング後にもう一回走らせる。まだグリーン。これは「リグレッションがない」を意味しない——「既知の挙動」が壊れていない、としか言えない。

コードパスにテストがないなら、Claude に最小のテストを書かせて現挙動をロックし、通して、コミットする。それからリファクタリング。そうしないと Claude がやっているのはリファクタリングじゃなくて書き直しだ——前後の等価性を検証する手段がない。

ガードレール #2:変更を原子的なコミットに分けさせる

今回のリファクタリングは実質 2 件:

  1. x402 バックエンド:自作 → gem
  2. Purchase / Subscription のフィールドマッピング:controller → model のクラスメソッド

フロントエンドには 3 件目:viem + x402-fetch で JS 側の署名フローを書き直す。

Claude には自然な境界で分けさせた:バックエンド + モデル抽出で 1 コミット(9f3e239)、フロントエンドは別コミット(93746d8)。各コミットに完全な説明、変更ファイル一覧、なぜ変えたかを載せる。

メリット:
- diff が読める。1 コミット 1 件。
- ロールバックの粒度が制御できる。本番でフロントエンドのバグが出たら git revert 93746d8 でフロントだけ戻せる、バックエンドは残る。
- Claude 自身の注意も集中する。1 コミット 1 件だと、Claude の注意もそのひとつだけに向く。

ガードレール #3:diff を先に読む

リファクタリング完了後、Claude を止めて git diff --staged を出させる。テストは走らせない、アプリも起動しない、まず diff を読む。

チェックするシグナル:

  • 何を削除した? app/services/x402/payment_handler.rb が丸ごと消えた——OK、gem 移行の本旨。だが触れと言っていないものを消していたら即座に問い詰める。
  • フィールドマッピングは変わっていないか? Purchase.create!(wallet_address: verify_result["payer"], ...)Purchase.record_x402!(payment:, settlement:) では payment[:payer]。ソースは変わった(gem の request.env vs 旧クライアントの戻り値)、でもフィールドは 1:1 で対応しないといけない。
  • 「ついで」の変更。Claude はリファクタリング中に「なんか変だな」と思った箇所をついでに直すのが大好き——エラーメッセージの言い換え、変数のリネーム、「これは切り出すべき」と思ったメソッドの抽出。ここは警戒。リファクタリングの約束は「挙動等価」だ。ついで修正がそれを崩す。

Claude が今回踏んだ 2 つの落とし穴

落とし穴 1:gem の Stimulus コントローラがサイレントにロードされなかった

x402-rails gem は自前の Stimulus コントローラを同梱する。Claude がコードを書いて、テストは全部グリーン。私が支払いボタンを手動でクリックしたら——無反応。

原因:config/importmap.rb にあった @hotwired/stimulus の pin が存在しない vendor ファイルを指していて、importmap がその pin を黙って破棄していた。結果、gem のコントローラは読み込まれない。テストでは捕まえられないbin/rails test は JS を実行しないから。

落とし穴 2:YAML が 0x... を integer としてパースした

wallet_address: 0x833589...——クォートなし。YAML は 0x で始まるので 16 進数の整数と解釈。読み出すと integer。Facilitator は非文字列を受け取って拒否。Claude は config を書く時に YAML のパース規則を意識しなかった。

どちらの落とし穴も実際にボタンをクリックして初めて発覚した。テスト通過 ≠ 機能が動く。リファクタリング後の手動検証はサボれない。

Claude リファクタリングの完全フロー

  1. 全テストスイートを走らせる、グリーン確認。対象コードに覆蓋がなければ、Claude に現挙動を固定する最小テストを書かせてコミット。
  2. 抽出したい surface を言語化する。「リファクタリング」が通じるのは重複が明白なとき。そうでないなら「X のフィールドマッピングを Y のクラスメソッドに抽出」と言う。
  3. 原子コミットに分ける。1 コミット 1 件。
  4. diff を自分で読む。何が消えたか、フィールドマッピングは成立するか、ついで修正はないか。
  5. テストを再度走らせる、グリーン。
  6. ユーザー可視のパスを変えたなら、機能を手でクリックして回る。JS、importmap、CDN、YAML パースなど、テストが届かない層は自分の目で確かめるしかない。

リファクタリングは「Claude に運転させる」中でも最もリスクが高いシナリオだ。ガードレールは Claude のためにあるんじゃない、あなたのためにある——Claude が外したときに、5 分で気づけるように。本番が燃えてから、ではなく。