綴Photo の勉強ノートに戻る
コードレビュー学習2026.06.11

AI レビューの指摘 20 件中、無修正で生き残ったのは 6 件だった — 反証検証という二段構え

コードレビューAI活用false positiveドキュメント綴Photo

Claude の多段レビュー(7観点の並列レビュー+反証検証)で浮かんだ「AI の指摘をどう信じるか」まわりの観察を、勉強のためにまとめたものです。

綴Photo モバイル版のコードベース全体を、Claude の多段レビューにかけた。アーキテクチャ・コード品質・データ層・テスト・セキュリティ・リリース準備・パフォーマンスの7観点で並列にレビューさせ、medium 以上の指摘が 20 件出た。

20 件全てを順に直そうとすると、深刻度の差が見えないまま等価に扱うことになります。そこで指摘を直す前に、「反証専任」のエージェントをもう一段挟んだ。各指摘について「実コードを読んで反証を試みよ。意図的な設計判断が文書化されていれば not-an-issue。迷ったら棄却に倒せ」と指示した。

結果、20 件のうち深刻度据え置きで確定したのは 6 件。残りは「事実は正しいが深刻度が誇張」が 12 件、「そもそも誤検出」が 2 件だった。修正対象が約 1/3 に絞れた。

指摘リストはレビューセッションのメモリ。保存しない。DB には無い。ソースファイルGit証拠はここ一段目の指摘 20件セッションメモリ・保存しない7観点の形探し実コードで潰す。迷ったら棄却確定 6件直す対象深刻度の誇張 12件事実は合っている誤検出 2件棄却20件はメモリ上のリスト。ファイルにも DB にも残らない。

この記事は、その「二段目」が何をしたのか、なぜ効いたのかをまとめる。深刻度ラベル自体の読み方(feature flag・throttle・設計意図で実害が変わる話)は別記事で扱ったので、ここでは触れない。

この記事で見ていく点

  • AI レビューの指摘は「事実認定」と「深刻度」を分けて受け取る必要がある
  • 偽陽性の正体:コードのパターンは実在するのに、害のメカニズムが成立しない
  • 「これは仕様」コメントや調査メモが、AI レビューを未然に黙らせる防御になる

事実は合っているが、深刻度が文脈を知らない

20 件の内訳で一番多かったのは「事実は正しいが深刻度が誇張」の 12 件だった。これは偽陽性とは少し違う。指摘されたコードの状態そのものは実在する。ただ、その深刻度ラベル(high / medium のような重み付け)が、個人開発であること・実害の有無・既に下した判断といった文脈を知らないまま付いている。

なぜ AI レビューは深刻度を盛りやすいのか。レビューする側は一般原則をベースに判断します。「同期処理を UI スレッドで回すのは high」「例外を握りつぶすのは medium」——どれも教科書的には正しいです。だが「そのコードが本番でどれくらい踏まれるか」「個人開発なので影響範囲はユーザー自分一人」みたいな個別の文脈は、コードの字面には書いていません。だから一般原則どおりの重みが付きます。結果として、実害から見ると一段重いです。

学びポイント

  • AI の指摘は「事実認定」と「深刻度評価」を分けて受け取る。事実は概ね正確、深刻度は文脈を知らないぶん盛られている
  • 「12 件は事実が正しい」を「12 件直さなきゃ」と読まない。深刻度を自分の文脈で割り引くと、多くは「いつか」「やらない」に落ちる

コードのパターンは実在したのに、害が成立しなかった

20 件のうち 2 件は、格下げではなく棄却になった。事実認定そのものが間違っていた、つまり偽陽性だ。一番きれいな実例がパフォーマンス指摘だった。

コードで起きていたこと(と、起きていなかったこと)

指摘はこうだった——「写真選択グリッドが、build のたびにサムネイル取得の Future を作り直している。だからタップごとに全セルが再読込される」。

これはもっともらしい。実際、綴Photo のコードには FutureBuilder の future: を build の中で生成している箇所がある。ざっくりこういう形だ。

dart
@override
Widget build(BuildContext context) {
  return FutureBuilder<Uint8List?>(
    // build のたびに新しい Future が作られる(アンチパターンの「形」はここ)
    future: asset.thumbnailDataWithSize(_thumbnailSize),
    builder: (context, snapshot) {
      // サムネイルを描画する
    },
  );
}
// ↑ このコードは「形」として実在する。
// ただし反証エージェントが追うと、このウィジェットがデッドコード or
// rebuild が走らない構造のどちらかに該当していた(次の段落で説明)。

future: を build の中で作るのは、Flutter では確かにアンチパターンとして知られています。build が走り直すたびに Future が作り直され、FutureBuilder がローディング状態に戻ってチラついたり、無駄な再取得が走ったりします。パターンとしては実在します。

ところが反証エージェントが実コードを追ったら、害のメカニズムが二重に成立しなかった。

  1. 指摘が名指しした対象ファイルの片方は、どこからも参照されていないデッドコードだった。呼ばれないので、害が発生しようがない。
  2. もう片方は、タップしてもそのセルの再描画(rebuild)が走らない構造になっていた。build が再実行されなければ Future も作り直されない。つまり「タップごとに全セル再読込」の起点が存在しない。
サムネイルの Future は画面のメモリ。保存しない。DB には無い。ソースに実在する形future: を build の中で作る。ファイル・Gitデッドコードどこからも参照されない呼ばれないので害が出ない生きているセルタップしても rebuild しないFuture は画面メモリ形は合っている。再読込の起点が無い。

なお、パフォーマンス上の害は無かったとはいえ、「どこからも呼ばれない widget が残っている」こと自体は別の小さな負債だ。なので害が無いと確認したうえで、このデッドコードの widget はその後削除した。指摘そのものは外れていても、掃除のきっかけにはなった形だ。

なぜこういう偽陽性が出るか

AI レビューは「危険なコードパターンの形」をよく知っています。build 内 Future、N+1 クエリ、try の外の return——こういう「形」を見つけるのは得意です。だが「その形が実際に害を生む経路に乗っているか」は、コードを 1 ファイル眺めるだけでは分かりません。呼び出し元をたどり、再描画が走る条件を確認し、デッドコードでないことを確かめる——ここまでやって初めて因果が確定します。

パターン検出と因果確認は別の作業で、AI の一段目レビューは前者に強く、後者を飛ばしがちです。だから「形は合っているが害はない」が出ます。

学びポイント

  • 偽陽性は「全く的外れな指摘」とは限らない。パターンは正しいのに因果が成立しない形で出る。これが最も棄却の判断が難しい
  • 「危険なパターンが見つかった」と「それが害を生む」の間には、呼び出し元の確認・再描画条件の確認・デッドコード判定という工程がある。ここを飛ばすと偽陽性を掴む
  • だから二段目(反証)は、一段目とは別の作業——「形探し」ではなく「因果の裏取り」——をやる必要がある

「これは仕様」コメントが、AI を未然に黙らせた

確定が 6 件まで絞れたのは、格下げと棄却のおかげだけではない。そもそも指摘として上がってこなかったものもある。コードやメモに「これは意図的だ」と書いてあったため、レビュアーがそれを読んで not-an-issue と判断したり、最初から指摘を出さなかったケースだ。2 つ実例がある。

実例1: OS 軸で画面を分ける、という設計判断

綴Photo は画面を mobile / desktop で分けている。ふつう「広い画面ならデスクトップ用レイアウト」と画面幅で分けることが多い。実際このアプリにも画面幅で分ける仕組みはある。

dart
class Breakpoints {
  static const double desktop = 800;
  // 画面幅が 800px 以上ならデスクトップレイアウト
  static bool isDesktopScreen(BuildContext context) =>
      MediaQuery.sizeOf(context).width >= desktop;
}

ところが「綴るモード選択」みたいな一部の画面だけは、画面幅ではなく OS で分けている。iPad や Android タブレットは画面が広くても、モバイル用の UI を出す。幅で判定する仕組みがすぐ隣にあるのに、こっちだけ OS で判定している——これは「判定基準がバラバラなバグ」に見える。レビュアーが指摘したくなる典型形だ。

だが、その画面の入口にこういう doc コメントが書いてある。

dart
/// デスクトップOS (Win/Mac/Linux): 3カード(前回の続き / フォルダ選択 / ファイル選択)。
/// モバイルOS (iOS/Android): 3カード(自動で選ぶ / 月・年で選ぶ / 自分で選ぶ)。
/// **タブレットも OS 軸で判定する** ため iPad / Android タブレットは
/// 画面幅が広くてもモバイル側のモード選択を出す(写真ピッカーも同じ方針)。これは仕様。

「タブレットも OS 軸で判定する。これは仕様」。この 1 行が、複数のレビュアーがこれをバグ指摘するのを未然に防いだ。判定基準がバラバラなのは事実だが、それは意図された不揃いだと、コードを読むだけで分かるようにしてある。

実例2: 「調査済み・実害なし」まで書いた DB のメモ

データ層では、写真テーブルに紐づく「気持ち」テーブルのインデックスについて、こういう疑問が出やすい——「新規インストールと既存ユーザーのアップデートで、インデックスがちゃんと両方に入るのか?」。マイグレーションは見落としが起きやすい領域で、「片方には入るがもう片方に入らない」は実際よくあるバグです。

だが、その列の定義の直前にこう書いてある。

dart
/// SQLite は FK 列に自動でインデックスを張らないため、photoId 単体のインデックスを明示する。
/// 詳細画面の watch(photoId で絞り込む)・本棚の join がインデックス無しだと全件走査になるのを防ぐ。
/// @TableIndex により createAll(新規インストール)にも入り、v9→v10 マイグレーションで既存 DB にも追加する。
@TableIndex(name: 'idx_meanings_photo_id', columns: {#photoId})

「なぜこのインデックスが要るか(FK 列に自動で張られないから/全件走査を防ぐため)」「どこで効くか(詳細画面・本棚)」「新規インストールにも既存 DB にも両方入る(createAll とマイグレーションの両方を通る)」——ここまで書いてある。だから「片方に入らないのでは?」という疑問は、コードを読んだ時点で潰れる。調査済みで、対応経路まで記録されているからだ。

なぜ「これは仕様」コメントが効くのか

AI レビューが指摘を盛るのは、文脈を知らないからでした(最初に見たとおり)。だったら、文脈をコードに書いておけば盛れません。

  • 「なぜこの不揃いが意図的か」を書いておけば、不揃いをバグと取り違えない
  • 「調査済み・実害なし・対応経路」を書いておけば、既知の判断を蒸し返さない
意図の置き場所。チャットは保存しない。チャットの合意セッションメモリあれは大丈夫保存しない次のレビューに無いソースの doc コメントファイル・Gitこれは仕様次のレビューが読む閉じても残るidx_meanings_photo_id端末 SQLite の Meanings新規は createAll既存は v9→v10閉じても残る次のレビューが読むのはファイルと DB だけである。

これは人間のレビュアー向けに昔から言われてきたことだが、AI レビューの時代だと意味が一段増します。意図的な判断を文書化することは、人間のためだけでなく、AI レビューに対する永続的な防御になります。

学びポイント

  • 意図的な「変な書き方」には「これは仕様」と理由を添える。レビューのたびに同じ指摘を受けて消耗するのを防げる
  • 調査して「実害なし」と判断したら、その結論と根拠(対応経路まで)をコードかメモに残す。次に読む AI も人も、同じ調査をやり直さずに済む
  • AI レビューは記憶を持たない。チャットでの合意は引き継がれない。コードに書いたものだけが資産になる

このテーマの括り

3 つに共通するのは、「AI の指摘を、一段目だけで信じない」という構えです。

  • 事実は正しくても深刻度は文脈を知らない → 自分の文脈で割り引く
  • パターンは実在しても因果は成立しないことがある → 呼び出し元まで裏取りする
  • 文脈をコードに書いておけば、そもそも指摘が湧かない → 文書化で先回りする

ポイントは、二段目に「迷ったら棄却に倒せ」と指示したことだ。検証役が「念のため残す」方向に倒すと、結局フィルタが効きません。反証役は反証に倒します。それで残った 6 件は「反証を試みても生き残った」ぶん、信頼して直せる。

おわりに

AI にコードを書かせ、AI にレビューもさせる開発だと、レビュー結果が大量に返ってきて、全部に対応しようとして疲弊しやすいです。だが指摘の質はバラバラです。事実が誇張されているもの、因果が成立していないもの、そもそも意図的なもの——これらを一段目のまま受け取ると、本当に直すべき 6 件が 20 件のノイズに埋もれる。

付き合い方は「全部直す」から「生き残ったものだけ直す」に変わった。文書化は一度書けば、レビューを回す数だけ効いてきます。


この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。