AI生成コードをどうレビューする?テストが緑でも反例と証拠で確かめる
Vibe Codingでコード生成は速くなりましたが、レビュー負担まで同じように減ったわけではありません。LLMは整ったdiffを素早く作れます。しかし、流暢な説明やテストの全成功だけでは、本番に出してよい実装だとは証明できません。
今回扱うのは、このブログプログラムで使うアニメーション付きターミナルSVG向けのビルド前バリデーターです。限定したXML/CSS規則、manifestのパスとSHA-256、8言語の記事が可視のデモ画像を参照しているかを検証します。固定canonical SVGは正例です。4種類の欠陥はマージ前の隔離テストで見つかり、証拠はこのバリデーターのオフライン範囲に限られます。
最初のテストは 8/8成功 でしたが、独立レビューで誤受理が2件見つかりました。最初の修正後は 10/10成功、さらに2件が見つかりました。最終固定版は主テスト 12/12成功。独立レビュアーが追加の攻撃プローブ4件を実行し、すべて拒否されました。JavaScript 3ファイルも node —check を通過しています。
範囲と不変条件を固定し、最小反例を作り、実際の失敗を確認できたテストだけをREDと記録し、正確なcommitで正例・負例を独立再検証し、リスクに応じて判断します。ロジックテストはbuildや対象プラットフォームの検証を代替しません。高リスク変更では範囲、Base/Head commit、不変条件、正例、反例、実際のコマンドと環境、未検証の境界、結論を証拠カードに記録できます。validatorはブログのビルド前コンポーネントであり、独立配布の汎用CLIではありません。レビュー時間の短縮も測定していません。
公開リポジトリには、サニタイズ済みの合成反例と実行可能な教材テストだけを置いています。このブログプログラムの本番用validator、修正コード、内部アーキテクチャは含みません。本文の8/8→10/10→12/12は元の工程で行ったオフライン検証の結果です。公開テストは4種類の欠陥パターンを再現するだけで、本番実装を複製していません。
なぜ8/8のテストが通っても不正な入力を受け入れたのか
テストが緑でも、現在の入力が現在のassertionを満たしただけです。すべての不正入力が拒否される証明ではありません。
最初の反例では、許可された画像と未許可の別記事リソース参照を同じ行に置きました。旧ロジックは許可された画像タグを一つ見つけると行全体を通しました。独立レビューは別slugに実在するSVGを作り、8言語の記事の正しい画像と同じ行にMarkdown参照を追加しました。存在しないファイルによる偽の失敗を避けるため、参照先も実際に作っています。旧実装は posts_checked=8 で成功しました。
<img src="/assets/terminal-demo.svg" alt="Terminal demo" width="356" height="137" /> 
2つ目はファイルシステム競合です。検査したパスと実際に開くファイルが異なる可能性があります。checkout内のパスには誤ったバイト列を置き、外部には期待SHA-256と一致するcanonical SVGを用意しました。親ディレクトリを外部へのsymlinkに置き換えると、旧実装は外部ファイルを読み誤って受け入れました。回避されたのはファイルの出所制約で、ハッシュ検証ではありません。
最終Linux実装は信頼されたcheckoutのルートを開き、開いた親ディレクトリのファイル記述子を基準に各要素を解決します。中間要素はディレクトリで O_NOFOLLOW を使い、最終ファイルもsymlinkを拒否します。型、リンク数、サイズを調べ、読み取り前後のdevice、inode、sizeを比較してからdigestを確認します。/proc/self/fd が使えなければfail closedです。検証範囲は信頼されたcheckoutとprocfsを前提とし、同一UIDやホスト権限の攻撃者は対象外です。Linux openat2(2)の説明も参照してください。
第2ラウンド:ハッシュは正しくても、内容が見えない
最初の修正後、テストは 10/10成功 しました。3つ目はreduced motionの最終フレームです。すべてのフレームが透明から始まり、通常のアニメーションで表示されます。reduced-motion設定でアニメーションだけを止め、最後のフレームを表示しなければ画面が空白になります。
.f { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
#f0 { animation: none; opacity: 1; }
#f3 { animation: none; } /* 最終フレームは透明のまま */
}
テストはSVGのSHA-256、サイズ、content-addressed filename、canonical manifestを再計算して周辺検証を維持し、「最終結果が見える」という契約を検証しました。MDNのprefers-reduced-motionを参照してください。
4つ目はMarkdownコメントです。旧実装は正規表現で画像タグを数え、HTMLコメントを除外していませんでした。唯一の画像をコメントにすると、ソースにタグが残ってもページには表示されず、旧ロジックは画像1枚と数えます。CommonMark 0.31.2のHTML block規則では、コメント内のタグは可視画像ではありません。
<!-- <img src="/assets/demo.svg" alt="Terminal demo" /> -->
テスト結果を正確なバージョンに結び付ける
| コード版 | 主テスト | 後続の独立レビュー |
|---|---|---|
| 初期版 | 8/8成功 | SEC-01、SEC-02の誤受理 |
| 第1回修正版 | 10/10成功 | SEC-03、SEC-04の誤受理 |
| 最終オフライン検証版 | 12/12トップレベルテスト成功 | 追加4プローブをすべて拒否 |
最終12件にはSEC-01〜04の回帰テストが含まれます。追加の4プローブは12/12に含まれず、16/16と合算してはいけません。JavaScript 3ファイルは node —check に通りましたが、構文検査は動作テストの代わりにはなりません。
後半2件の回帰テストはソース修正より前にコミットされましたが、テストだけのcommitで全テストは実行されていません。したがって完全な10/12 REDの記録はありません。確認できるのは、旧版の完全なvalidatorで誤受理が再現され、最終版の主テストと追加プローブが通過したことです。
動作の証拠が揃ってから複雑度を確認します。helper、分岐、設定、呼び出し元が何の不変条件を守るのか、重複か、実際に使われているかを追います。コードが短いほど単純とは限らず、パス保護を削ると競合が再発する可能性があります。
インタラクティブ証拠パネル
合成反例を選び、入力、不変条件、再現される失敗と証拠の範囲を確認します。
選択中のケース: SEC-01 · 存在は排他性を意味しない
SEC-01 · 存在は排他性を意味しない
- 入力条件
- 許可画像と別記事への未許可参照が同じ入力行にあります。
- 守るべき不変条件
- 関連するすべての参照が規則を満たす必要があり、1つの許可参照だけでは安全を証明できません。
- 合成例で再現する失敗
- 旧ロジックは許可画像を見つけると行全体を受け入れ、追加参照を見落としました。
- 期待する正しい動作
- すべての参照を検査し、境界外の参照があれば拒否します。
- 元のレビュー段階
- 第1回の独立レビューで発見
- 応用できるレビューの教訓
- 有効・無効な内容を含む最小反例を作り、全入力を検査するか確認します。
SEC-02 · 検査と使用の競合
- 入力条件
- パス検査後、開く前に親ディレクトリやシンボリックリンクを差し替えます。
- 守るべき不変条件
- 検証対象とアクセス対象は一致し、意図したファイルシステム境界を守る必要があります。
- 合成例で再現する失敗
- 検査後にパスを開き直すと、その間に対象を変更される可能性があります。
- 期待する正しい動作
- 検証とアクセスで境界を維持します。この合成例は本番修正を実装した主張ではありません。
- 元のレビュー段階
- 第1回の独立レビューで発見
- 応用できるレビューの教訓
- 検証から使用までに他者が変更できる状態がないか調べます。
SEC-03 · 動きを減らしても最終フレームを表示
- 入力条件
- SVG フレームは透明で始まり、動きを減らす設定ではアニメーションが停止します。
- 守るべき不変条件
- アニメーションを止めても重要な最終コンテンツを隠してはいけません。
- 合成例で再現する失敗
- 最終フレームを表示せずに停止すると画面が空白になることがあります。
- 期待する正しい動作
- 動きを減らす設定でも完全で読みやすい最終状態を直接表示します。
- 元のレビュー段階
- 第2回の独立レビューで発見
- 応用できるレビューの教訓
- アクセシビリティ設定を独立入力として扱い、内容の可視性を確認します。
SEC-04 · コメントは可視内容ではない
- 入力条件
- HTML コメント内に画像要素のような文字列があります。
- 守るべき不変条件
- ソース文字列とブラウザーが実際に描画する要素は異なります。
- 合成例で再現する失敗
- 単純なタグ数はコメント内の画像を可視画像として数えることがあります。
- 期待する正しい動作
- コメントノードを除外して構造を解析し、実際の要素だけを判定します。
- 元のレビュー段階
- 第2回の独立レビューで発見
- 応用できるレビューの教訓
- 偽のタグをコメントに入れ、構造を検査しているか確認します。
公開された合成教材の証拠
公開リポジトリは合成した欠陥パターンのみを再現します。テスト成功は失敗の再現を意味し、本番修正の成功を意味しません。
固定版の合成教材リポジトリを開く元の工程のオフライン要約
元の記録:8/8 → 10/10 → 12/12。独立した4つのプローブは別に実行され、12/12には含まれません。
検証範囲
対象は Node.js v22.16.0 / Linux x64 のオフライン検証器です。ネイティブ macOS の並行書き込みとサイト全体の実行時確認は未検証です。
関連記事:Mutation Testing、OpenQodexのpre-pushレビューのfail-open境界、Diffleのレビューコメントアンカー。
テスト通過から本番承認までに残ること
最終独立検証は Node.js v22.16.0、Linux x64のオフラインvalidatorを対象にしました。主テスト12/12成功、追加4攻撃プローブはすべて拒否、canonical SVG・manifest・8言語の正例が成功し、JavaScript 3ファイルも node —check を通過しました。
これは固定ルールと対象環境で4種類の既知の誤受理が修正されたという限定的な結論を支えます。ブログ全体の本番準備完了を証明するものではありません。悪意ある並行書き込みに対するネイティブmacOSの動作は未検証で、サイト全体のbuildと最終ページの実行確認もオフライン試験の範囲外です。
実際のリリース判定はリスクごとに分けます。単体テスト、静的検査、buildと統合テスト、対象プラットフォームと実行時確認は、それぞれ異なる境界を検証します。重要な境界が未検証なら、局所的な成功を全体の本番準備完了と表現してはいけません。
契約と反例で疑うべき動作を見つけ、正確なバージョンのテストと独立証拠で次に深く読む場所を決める。人の判断をなくすのではなく、「問題なさそう」を証拠と範囲が明確なレビュー判断に変えるのです。