検索/大きなコードベースで、その場しのぎをせず原因を直し、公開APIの互換性を守る

大きなコードベースで、その場しのぎをせず原因を直し、公開APIの互換性を守る

AIに大きな多層のコードベースを直させる時に使う。ビルドのログを短く抑え、バグを上流の原因までたどって直し、自分の差分にその場しのぎのヘルパーが無いか見直し、分かりやすいプルリクエストを書く。

このAIルールを使うには
## ビルドのログを短く抑え、コミット前にフォーマッタとテストを回す - ビルド時は、すべてのビルド出力をヌルデバイスへリダイレクトし、失敗した場合のみ同じコマンドを詳細モードで再実行する。こうすることで、成功したビルドのログにトークンを消費せずに済む。例: `build-command >/dev/null 2>&1 || build-command` - ネットワーク経由の取得よりローカルにキャッシュされた依存関係を優先し、利用可能であればビルドキャッシュやコンパイラキャッシュを有効にして、増分ビルドを高速化する。 - 長時間の無人実行やエージェント主導のビルドでは、設定時にモーダルの中断ダイアログやアサーションダイアログを無効化し、人間の対応待ちで実行がブロックされないようにする。 - コミットのたびにフォーマッタを実行する。ファイルを変更せずに検証したい場合はチェック専用モードを使う。プロジェクトのスタイルに沿わないコードは、避けられるはずのレビューのやり取りを余計に発生させる。 - テストスイートはリポジトリのルートから実行し、回帰テストはプロジェクトのtestsディレクトリ配下にファイルとして追加する。GPUや特殊なハードウェアが利用できない場合は、カバレッジを省略するのではなくCPUモードやインタプリタモードでテストする。 - クロスシェル環境(例えばUnixシェルからホストのツールを呼び出す場合など)では、正しい実行可能拡張子を付加し、意図したプラットフォームのバイナリが実行されるようにする(誤ったプラットフォーム用のバイナリが実行されないようにする)。 ## ガードで隠さず、上流にある本当の原因を直す - 最小限の差分よりも大きな作り直しが必要になったとしても、構造として正しく堅牢な表現を目指す。 - 症状ではなく根本原因を直す。出力生成のような後段のフェーズでバグが表面化した場合、原因は通常もっと上流にある——それより前の変換、正規化、特殊化、下位変換、あるいはデータ表現そのものだ。原因をそこまで遡って突き止め、そこで修正する。 - 変更を残す前に、その一つひとつを問い直す。なぜ必要なのか、それがないとどのテストが失敗するのか、これは正しい修正なのか、それとも表現そのものに欠陥があるというシグナルなのか。変更がないと失敗するテストを具体的に挙げられないなら、その変更はおそらく存在すべきではない。 - 問題を覆い隠さない。不正な構造を取り繕うガード処理・nullチェック・特殊ケースは、表現上のバグを隠しているにすぎない。正しい入力では一度も通らないガードはデッドコードだ。表現そのものを正しくすることで、下流の利用側をシンプルに保つ。 - 入力の形を厳しく問い直す。特定の形の入力を処理するときは常に、その形自体が正しいのか、それとも上流の生成元を直すべきなのかを問う。その場で対処してよいのは、その形が本当に正当な入力である場合だけだ。 - 編集距離の小ささよりも正しい表現を優先する。二つの表層形式が本来等価であるべきなら、同一のモデルとして扱う。また、概念上順序のないキーと値のデータには、位置やインデックスではなくキーや役割でアクセスする。 - 作業を通して作業ログを残す——具体例を添えた問題の説明、ブロッカー、ある修正が次の修正へと連鎖していく様子、選んだ修正とそれが原則に沿っている理由、そして却下した代替案。これを要約して変更の説明文に反映し、コミットするコード自体には含めない。 ## 自分の差分に、悪い設計を取りつくろうだけのヘルパーが無いか見直す - 些細でない変更を確定させる前に、差分を見直し、それが悪い表現を取り繕っているだけの兆候がないか確認する。そのパターンがこの層に本当に属すると証明できるまでは、危険信号として扱う。 - 新しく再帰的な等価判定関数やマッチ判定関数のような、独自のセマンティック等価性ヘルパーは、しばしば二つの代替表現が並存することを許してしまったサインだ。まず、既存の正規化・置換・等価性のパスがなぜそれらをすでに同一にしていないのかを問うべきだ。 - 監査されないままヘルパーが増えていくこと。新しいヘルパー・フォールバック・try系の関数はすべてレビュー対象として扱う。とりわけ、単一の失敗テストを通すためだけに存在するものや、既存の仕組みの一部を再実装しているものは要注意だ。 - すでにチェック済みの意味データから表層の構文を再構築する「意味から構文への再構成」は、たいてい生成元が誤った表現を保持してしまっているサインだ。チェック済みの意味的な値こそが信頼できる唯一の情報源であるべきだ。 - オペランド・参照・所有関係のチェーンをたどって引数・キー・親を復元するような、グラフ走査によるコンテキストの再発見は、たいてい下流での取り繕いだ。代わりに生成元で正準形を組み立てるべきだ。 - 利用側でのパッチ当て。後段のフェーズが前段のフェーズから来た不正な形を修正すべきではない。また、表現に関する細かいハードコード(特定のサブクラス向けの特殊ケース、マジックネーム、引数のインデックス、エントリの順序など)には強い不変条件が必要であり、たいていは正準な構築境界に属すべきものだ。 - ありえない形を黙って処理すること。契約外の形に対してデフォルト値を返すガードはバグを隠してしまう。静かに受け入れるのではなく、ありえない形についてはアサートすべきだ。 - レビューはまず「疑わしいヘルパーの棚卸し」から始める。新しいヘルパーごとに、それが既存のどの仕組みと重複しているか、それがないとどのテストが失敗するかを一覧化する。フラグを立てた変更ごとに、正確な入力の形とその生成元を特定し、その形が正準なものか偶発的な代替物かを判断し、まず生成元での修正を試み、既存の信頼できる情報源を特定し、そして「差し戻しドリル」を行う——変更を取り除き、最小の失敗テストを実行し、本当の破綻箇所をたどる。テストを通すという理由だけでフラグの立った変更を残してはならない。 ## 例つきのコメントを書き、同じ情報は1か所だけで持つ - 関数のコメントは完全な文で書く。まずその関数が何をするかを述べ、自明でない場合はなぜ存在するのかも書き、些細でない挙動には断片的な説明ではなく具体例を添える。 - コメントや説明では会話調の具体例を使う。抽象的なラベルを説明の代わりに使うのではなく、「次の例を考えてみよう」として実際のコードを示し、何が起きるか、どのパスがその形を作るのか、ローカルのコードがどの不変条件を守っているのか、どの利用側がそれに依存しているのかを順を追って説明する方がよい。 - 書く前にまず再利用できないか探し、些細でないロジックは名前付きでドキュメント化されたヘルパーへ切り出す。まず共有ユーティリティを探し、本当に新しいロジックである場合は、意図が伝わる名前とドキュメントコメントを付ける。複数ステップの計算をインラインブロックに埋め込んだままにしない。 - 信頼できる情報源をひとつに保ち、リファクタリング後はデッドコードを削除する。あるものをマッピングしたり分類したりする場所はちょうど一箇所にする。変更によって到達不能になった分岐やヘルパーは、残さずに削除する。 - 値ごとに正準な表現をひとつだけ保ち、その不変条件をアサートする。同じ論理的な値に対する二つ目の表現は、等価性・同一性・重複排除を壊してしまう。ある不変条件によって特定の入力に対してその形が決して生成されないと保証される場合は、構築の場所でアサートする。 - 契約外の入力に対しては大きな声で失敗する。ヘルパーが限られた入力の集合に対してのみ有効である場合、その範囲外の入力に対してはデフォルト値を黙って返すのではなく、アサートする。 ## 回帰テストを付け、5つの部分に分けてプルリクエストの説明を書く - コードをフォーマットしたうえで、変更にラベルを付ける。デフォルトでは非破壊的な変更とし、バイナリインターフェースや言語の挙動を変える場合は破壊的な変更としてラベル付けする。 - 修正前であれば失敗していたはずの回帰テストを含める。 - 説明文は固定の5部構成で書く。動機(具体例を添えた問題の説明)、提案する解決策(アプローチとそれが原則に沿っている理由)、変更のまとめ(触れたファイルや領域とそれぞれが何をするかの一覧)、概念と用語(よく知られた基本用語ではなく、そのレポートが依存するコードベース固有・微妙な用語だけを簡潔にまとめた用語集)、そしてプロセスレポートだ。 - プロセスレポートでは、すべての変更に論理的な理由を添えて説明する。連鎖的な問題については、単なる説明ではなく、それを動機づけたテストと、関与する具体的な関数名を挙げたコードのトレースを示す。入力の形をガードしたり特殊扱いしたりする変更については、その形が正当な入力なのか、それとも生成元を直すべきだったのかを明記し、レビュアーがその修正が正しい層にあるかを確認できるようにする。 - 自分と同じ文脈を持たないレビュアーに向けて書く。具体例から始め、型や関数の名前を挙げるだけでなく関連するコードスニペットを含め、手順を順序立てて説明する。 ## 公開APIとABIを壊さないよう、enumとインターフェースは末尾に足す - 公開APIのヘッダへの変更はすべて、古いバージョンに対してすでにコンパイルされている呼び出し側とのバイナリ互換性・ソース互換性を保つ必要があるものとして扱う。 - 列挙型(enum)について。既存のenumの途中に新しい列挙子を挿入してはいけない。挿入するとそれ以降のすべての値がずれてしまい、値を保存または比較していた呼び出し側を静かに壊してしまう。新しい列挙子は、終端のカウントやセンチネルメンバーの直前に、明示的な次の連番の値として追加する。列挙子を廃止する場合は、元の整数値を保ったまま「削除済み」を示す名前にリネームする。廃止した値を再利用してはいけない。 - COMスタイルのインターフェースにおける仮想メソッドテーブルは、宣言順によってレイアウトが固定される。そのため、仮想メソッドの順序を変更してはならず、メソッドのシグネチャや呼び出し規約を変更してはならず、途中に新しいメソッドを挿入してはならない(閉じ括弧の直前、末尾に追加する)。また、メソッドを削除してもいけない。その代わり、本体を未実装のスタブに置き換え、宣言自体は残す。 - クライアントがそのインターフェースを実装したり識別子で問い合わせたりする可能性がある場合、既存の公開インターフェースをその場で拡張することは避ける。代わりに、独自の識別子を持つ新しい派生インターフェースやバージョン管理されたインターフェースを追加し、既存の呼び出し側のために元のインターフェースはそのままサポートし続けるほうがよい。