0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに書かせたコードを保守不能にしないために、効いた対策を効果が大きい順に並べた

0
Posted at

半年前にAIと作ったプラグインを開いて、最初に気づいたのは、日付を整形する処理が3か所にあったことでした。

ユーティリティ関数に1つ、クラスのメソッドに1つ、テンプレートに直書きで1つ。どれも少しずつ書き方が違う。報告された表示のずれは、そのうちの古い1つが原因でした。直そうとして、どれを直せば済むのか、全部直すべきなのか、判断するだけで時間が溶けていく。八割をAIに任せて、速く作ったコードでした。仕様書は書いていません。

この記事は、その状態を繰り返さないために変えたことを、効いた順に並べたものです。エージェントを使うのをやめる話ではありません。やめずに速さを保ったまま、半年後の自分が保守できる状態にするための、具体的な手当てだけを書きます。

検証環境

設定やファイル名はバージョンで変わります。下記は実機で確認した時点のものです。最終的には自分の環境で /permissions/config、各ツールの設定ファイルを開いて挙動を確かめてください。

  • Claude Code: claude --version の値(公開時に実機の番号へ)
  • Codex CLI: codex --version の値
  • Node.js / PHP 8.3 / WordPress(対象はプラグインとテーマ)
  • 永続指示ファイル: Claude Code は CLAUDE.md、Codex CLI は AGENTS.md

以下は、八割をAIに任せて一度こけた人間が、その反省から組み直した運用です。万能のやり方の紹介ではなく、同じ失敗を避けたい人へのメモとして読んでもらえると距離感が合うと思います。

なぜ半年で保守不能になるのか

対策の前に、原因を3行だけ。速いこと自体は悪くありません。速さと引き換えに抜け落ちるものが、後で効いてきます。

設計の理由がチャットの中だけに残り、ウィンドウを閉じると消える。差分を読まずに受け入れるので、コードと自分が馴染む時間がゼロになる。構造をセッションごとにAI任せにするので、似た処理が違う書き方で散らばる。冒頭の「日付処理が3か所」は、3つ目の典型でした。

このあとの対策は、全部この3つのどれかを塞ぐためのものです。効果が大きかった順に並べます。上から2つだけでも、半年後の自分はかなり楽になります。

1. 判断を、リポジトリに着地させる

いちばん効きました。チャットに流れて消えていた「なぜ」を、コードと同じ場所に残す。やっているのは2つです。

ひとつは、CLAUDE.md の使い方を変えたこと。AIへの指示書としてだけでなく、後半に「決めたこと」のセクションを足して、設計判断を数行ずつ書きます。何をしたかではなく、なぜそうしたか。そして、やめた案とその理由も書く。このやめた理由が後でいちばん効きます。半年後の自分は、良さそうな別案を思いついて作り直し、同じ穴にもう一度落ちるからです。

## 守ること(AIに読ませる規約)
- 関数・フック・オプションキーは接頭辞 `tmfs_` を付ける
- 公開APIを変える提案は、実装の前に必ず止めて確認させる

## 決めたこと(未来の自分に読ませる記録)
- Webhook の署名検証は、ライブラリではなく hash_equals で自前実装した。
  依存を増やしたくなかったのと、検証ロジックを目視で追える状態にしたかったため。
- 署名が不正なリクエストは 400 ではなく 200 を返してログだけ残す。
  攻撃者に検証の成否を教えないため。当初 400 にしていたが、上の理由で変えた。

指示と記録が同じファイルだと見出しで混ざるので、前半を「守ること」、後半を「決めたこと」に分けています。AIが後半を読んでも害はなく、過去の判断を踏まえた提案が返ってくることもあるので、同居の利点もあります。

もうひとつは、機能を1つ足すごとに docs/decisions.md に1段落だけ決定ログを残すこと。完成後にまとめて仕様書を書くのは、一度やって続かなかったのでやめました。終わってから書く仕様書は嘘が混じるうえ、腰が重くて先送りになります。1段落なら、機能を閉じた勢いのまま書けます。

### 2026-06-05 レート制限
- 外部APIへの送信を1分20件に制限。先方の上限が30件/分で、余裕を持たせた。
- AIは「制限は不要」と書いてきたが、過去にキューが詰まった事故があるので入れた。
- 迷い: 件数を設定画面で可変にするか。今は固定。

AIに「決定ログを書いて」と頼むと、それらしい文章が返ってきますが、コードから逆算したもっともらしい説明であることが多いです。本当の理由とは、似ているようでずれます。下書きは叩き台として受け取り、実際に自分が何を考えて決めたかに直してから残してください。半年後に効くのは、本当の理由の方だけです。

2. レビューの関所を、少数に絞る

差分を読まずに受け入れる癖を直すのに、設定からも手を入れました。

以前 settings.jsondefaultModeacceptEdits にした話を書きました(settings.json の過去記事)。確認が減って快適になる設定ですが、保守性の面では「読まずに進む」を後押しします。反動で全部読もうとしたら、速さの意味がなくなって半日で挫折しました。極端は続きません。

そこで、必ず自分が読む場所だけを少数に絞りました。

## レビューの関所(必ず人間が読む)
- 公開API(フック名、関数シグネチャ、REST ルート)を変える差分
- DB スキーマ、テーブル、オプションキーを変える差分
- 認証・権限・サニタイズ・エスケープに触る差分
- 1コミットで概ね 80 行を超える差分
これ以外(内部リファクタ、テスト追加、コメント)は acceptEdits に任せる。

選んだのは、後から取り返しのつきにくいものだけです。境界、データ、安全、それに大きすぎる差分。フック名が静かに変わると依存先が壊れ、エスケープが1つ抜けると半年後にセキュリティ指摘で気づきます。内部リファクタやテスト追加は、間違ってもダメージが小さいか、テストが拾う。ダメージの大きさで線を引くと、読む量が現実的になって、かえって続きました。

3. 構造と命名は、人間が固定する

冒頭の「3か所に同じ処理」は、構造をセッションごとにAIへ丸投げした結果でした。AIは放っておくと広げます。新しい関数を作り、新しいファイルを切り、前のヘルパーがあるのに似たものをもう1つ作る。

枠だけ人間が固定して、その中で動いてもらう形にしました。AGENTS.md(Codex CLI 用)と CLAUDE.md の両方に、同じ規約を書きます。片方だけだと、ツールを変えた瞬間に流儀が変わるからです。

## 命名と構造の規約
- トップ階層は admin / public / includes の3つ。新設しない。
- 新しいクラスを足す前に、既存の似たクラスがないか必ず確認する。
- 1ファイル1責務。500行を超えたら分割を「提案」する(勝手に分割しない)。

効くのは、新設を勝手にさせないことです。分割は提案にとどめさせる。勝手に分割されると、目的のコードがどこに行ったか分からなくなります。WordPress プラグインなら接頭辞の規約は元々あるので、頭の中の暗黙ルールを文字にして書き写すだけでも、最初の一歩になります。暗黙のままだと、AIにも未来の自分にも伝わりません。

4. コメントとコミットには「なぜ」だけ置く

AIに書かせるとコメントが増えます。しかも多くが「何をしているか」のコメントで、これは半年後の役に立ちません。コードを読めば分かることの二重化で、直し忘れるとコードとずれて嘘になります。

レビューで「何を」コメントは間引き、代わりにコードを読んでも分からない「なぜ」を継ぎ目に足します。

// 署名検証は hash_equals を使う。== だとタイミング攻撃で
// 1文字ずつ突破される余地があるため。
if ( ! hash_equals( $expected, $given ) ) {
    return;
}

なぜ == ではダメなのか。これはコードのどこにも書いていない判断です。半年後の自分が「== で十分では」と思って戻そうとしたとき、この2行が止めてくれます。AIへの指示も「コメントをつけて」ではなく「自明でない箇所にだけ理由のコメントを。動作説明は不要」に変えると、ノイズが減ります。

コミットメッセージも同じ置き場所です。AIに任せると「Fix bug」のような、diffを見れば分かる一行になりがち。一行目は何をしたか(任せてよい)、本文に、なぜを一文だけ自分で足します。

fix: 為替取得をタイムアウト3秒で打ち切る

決済画面が為替API待ちで固まる報告のため。
正確なレートより、画面が返ることを優先する判断。

git blame でこの行に来たとき、なぜ短いタイムアウトなのかがその場で分かります。コードとコメントと決定ログとコミット。「なぜ」を残せる場所は複数あって、変更にいちばん近いところに一文あれば十分です。

5. テストを「読める仕様」にする

仕様書が続かないなら、テストに仕様を兼ねさせます。やっているのは、テスト名を「何を検証するか」ではなく「どう振る舞うべきか」で書くこと。

// before
public function test_verify() { ... }

// after
public function 署名が不正なリクエストはログだけ残して握りつぶす() { ... }
public function 送信失敗が3回続いたら諦める() { ... }

PHPUnit はメソッド名に日本語が使えます。テスト一覧を上から眺めるだけで、このプラグインが何を約束しているかが読める。しかもこの仕様は、振る舞いが変わればテストが落ちるので、放置してずれる心配がありません。

AIに書かせるときも「カバレッジを上げて」ではなく「守るべき振る舞いを、振る舞い基準の名前のテストに」と頼む。内部をなぞるテストはリファクタのたびに壊れて、いずれコメントアウトされ墓場になります。外から見た約束を確かめるテストは、内部が変わっても生き残ります。半年後に動いていたのは後者だけでした。

WordPress は本物のDBやフックが絡んで単体テストにしづらい部分もあります。そこは無理せず、WP-CLI で叩く手順や管理画面の確認手順を docs/ に箇条書きで残す逃げも使っています。テストにできないものは、手順として読める形に。形は問わず、半年後に再現できることだけ目指します。

6. 依存・バージョン・READMEは、軽く一行ずつ

ここからは軽いけれど効いたもの。

依存を1つ足すたびに、決定ログへ理由を一行。AIは最新を入れがちですが、最新が安定とは限らず、プラグインの利用者は古いPHPのこともあります。「PHP 8.0 でも動かしたいので 8.1 以降の構文を使うライブラリは避けた」のような外側の事情は、コードを読んでも分かりません。入れなかった理由も書くと、半年後に同じ検討を繰り返さずに済みます。

READMEの先頭には、未来の自分への案内を数行だけ。

## このプロジェクトについて(半年後の自分へ)
- Stripe 決済後にお礼メールを送る WordPress プラグイン。
- まず読むなら includes/class-tmfs-mailer.php の send() から。
- 設計判断は CLAUDE.md の「決めたこと」と docs/decisions.md。
- 事故りやすいのは送信の二重起動。cron に逃がした理由は decisions.md。

何のコードで、どこから読み始め、判断はどこにあり、注意点はどこか。この4行だけ。森に入る前の地図です。AIに任せると網羅的な説明を作りますが、網羅は体力が要って読まれません。短い案内の方が、再訪時には効きました。

補助: もう一体のエージェントに説明させる

比較ではなく、保守性を上げる道具としての併用です。Claude Code で書いたコードを Codex CLI に「説明して」「保守の弱点を指摘して」と読ませます(逆向きも同じ)。書いた本人は意図が見えるぶん説明が甘くなりますが、意図を知らない別のエージェントは、半年後の自分に近いまっさらな視点で読みます。説明が詰まる箇所は、未来の自分も詰まる箇所。そこがドキュメントの足りない場所です。

実際、「このフックの実行順序に依存しているが、前提がコードから読めない」と指摘されて、ここはコメントが要ると気づいたことが何度もあります。

別のエージェントの説明も事実とは限りません。「キャッシュを使っている」と説明されて実際は使っていなかった、ということもありました。説明や指摘は、自分が見落とした観点を拾う叩き台として使い、内容は自分で確かめてください。二体使うのは答えを二倍もらうためではなく、二つ目の視点を借りるためです。

長いセッションを閉じる前に、エージェント自身へ「このセッションで決めた設計判断と理由だけを decisions.md に箇条書きで。実装説明は不要」と頼んで、判断を書き出させる締めも挟んでいます。会話が消える前に、消えてはいけないものだけ陸に上げる感覚です。これも出力はそのまま貼らず、自分の記憶と照らして直します。

最低限、これだけ

全部を毎回はやりません。重いので。規模が小さくても飛ばさないのは2つです。

  • 判断を残す(CLAUDE.md の「決めたこと」と decisions.md の1段落)
  • 関所だけは読む(境界・データ・安全・大きな差分)

残りのテスト整理、コメントとコミットの「なぜ」、依存やREADME、別エージェントでの説明は、プロジェクトが育って一人で抱えきれなくなったら足す、くらいの温度でやっています。最初から完璧にやろうとすると、どれも続きません。

やってみて、続かなかったこと

正直に、捨てたものも書いておきます。完成後にまとめて書く仕様書は、嘘が混じるうえ腰が重く、何度も先送りして結局書きませんでした。差分を全部読むのは、速さと両立せず半日で挫折。きちんとしたADRテンプレートは、様式を思い出すのが面倒で三日でやめ、箇条書き数行に崩したら続きました。毎コミットに重い理由を書くのも気負いすぎて、いまは後で気になりそうな変更にだけ書いています。

並べると共通点があります。重すぎたか、完璧を目指しすぎたか。続いたのは、どれも「ほんの少し手を止める」程度に軽くしたもの。手抜きに見えるくらい軽くすると続く、というのが半年やって一番腑に落ちたところです。

次の自分に渡すメモ

速さを全部捨てるのではなく、継ぎ目だけ手を止める形に落ち着きました。判断するとき、境界を変えるとき、機能を1つ閉じるとき。その瞬間だけ速さを緩めて、なぜを一行残す。あいだの作業はエージェントに任せて速く進む。効くところだけ遅くする、という緩急です。

おまけで気づいたのは、「なぜ」を残す習慣がつくと、AIへの指示そのものも良くなったこと。自分が何を決めようとしているかを言葉にできるようになると、渡す指示も曖昧でなくなります。未来のための記録が、いまの速さも少し底上げしてくれる。これは始める前には思っていませんでした。

冒頭の3か所に散った日付処理は、まだ1か所にまとめていません。直すか、静かに役目を終わらせるか決めかねたままです。ただ、いま作っているものは、半年後の自分が迷わず歩けるようにはなってきた気がしています。読めなくなった家に鍵だけ持って立ち尽くすのは、一度で十分でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?